Compare commits

...

9 Commits

Author SHA1 Message Date
SagarRajput-7
d35db1b02b chore(test): refactored custom domain and generel settings tests 2026-05-14 06:55:07 +05:30
SagarRajput-7
0556e67739 chore(test): refactored memebrs and sa tests 2026-05-14 06:36:46 +05:30
SagarRajput-7
79ea20b371 chore(test): fixes in roledetailspage test 2026-05-14 05:45:13 +05:30
SagarRajput-7
2bbd11c181 chore(test): fix, unskip and refactor platform surfaces tests 2026-05-14 05:38:06 +05:30
SagarRajput-7
0f9891d1c1 chore(test): fix, unskip and refactor platform surfaces tests 2026-05-14 04:33:46 +05:30
Ashutosh Sharma
b236a29a99 fix(time-picker): disable browser autofill on time selection input (#11247)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
Browser autofill was interfering with the custom time selection input.
Added autoComplete='off' to prevent browser from suggesting values.

Fixes #5875
2026-05-13 20:24:35 +00:00
Jatinderjit Singh
828459ab30 fix missing icon for nodata alerts (#11292) 2026-05-13 19:44:58 +00:00
Jatinderjit Singh
b572e30045 fix(alerts): invalidate rule cache after disable/enable toggle (#11295)
* fix(alerts): invalidate rule cache after disable/enable toggle

The toggle calls patchRulePartial but never invalidated the
useGetRuleByID react-query cache. With refetchOnMount disabled on
the edit page, saving the form sent back the stale disabled value
and reverted the toggle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* reuse existing function to invalidate rule cache

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 19:42:36 +00:00
Piyush Singariya
f1ce804629 fix: remove returnSpansFrom from rawexport e2e (#11278)
* fix: remove returnSpansFrom from rawexportE2E

* fix: make returnSpansFrom optional

* fix: test assertion
2026-05-13 13:27:50 +00:00
15 changed files with 182 additions and 326 deletions

View File

@@ -596,6 +596,7 @@ function CustomTimePicker({
>
<Input
ref={inputRef}
autoComplete="off"
className={cx(
'timeSelection-input',
inputStatus === CustomTimePickerInputStatus.ERROR ? 'error' : '',

View File

@@ -1,6 +1,6 @@
import { GetHosts200 } from 'api/generated/services/sigNoz.schemas';
import { rest, server } from 'mocks-server/server';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
import CustomDomainSettings from '../CustomDomainSettings';
@@ -44,18 +44,20 @@ const mockHostsResponse: GetHosts200 = {
};
describe('CustomDomainSettings', () => {
beforeEach(() => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
});
afterEach(() => {
server.resetHandlers();
mockToastCustom.mockClear();
});
it('renders active host URL in the trigger button', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
render(<CustomDomainSettings />);
// The active host is the non-default one (custom-host)
@@ -63,20 +65,11 @@ describe('CustomDomainSettings', () => {
});
it('opens edit modal when clicking the edit button', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
);
fireEvent.click(screen.getByRole('button', { name: /edit workspace link/i }));
expect(
screen.getByRole('dialog', { name: /edit workspace link/i }),
@@ -89,28 +82,20 @@ describe('CustomDomainSettings', () => {
let capturedBody: Record<string, unknown> = {};
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
rest.put(ZEUS_HOSTS_ENDPOINT, async (req, res, ctx) => {
capturedBody = await req.json<Record<string, unknown>>();
return res(ctx.status(200), ctx.json({}));
}),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
);
fireEvent.click(screen.getByRole('button', { name: /edit workspace link/i }));
// The input is inside the modal — find it by its role
const input = screen.getByRole('textbox');
await user.clear(input);
await user.type(input, 'myteam');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
fireEvent.change(input, { target: { value: 'myteam' } });
fireEvent.click(screen.getByRole('button', { name: /apply changes/i }));
await waitFor(() => {
expect(capturedBody).toStrictEqual({ name: 'myteam' });
@@ -119,9 +104,6 @@ describe('CustomDomainSettings', () => {
it('shows contact support option when domain update returns 409', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
rest.put(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(
ctx.status(409),
@@ -130,18 +112,14 @@ describe('CustomDomainSettings', () => {
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
);
fireEvent.click(screen.getByRole('button', { name: /edit workspace link/i }));
const input = screen.getByRole('textbox');
await user.clear(input);
await user.type(input, 'myteam');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
fireEvent.change(input, { target: { value: 'myteam' } });
fireEvent.click(screen.getByRole('button', { name: /apply changes/i }));
await expect(
screen.findByRole('button', { name: /contact support/i }),
@@ -149,24 +127,14 @@ describe('CustomDomainSettings', () => {
});
it('shows validation error when subdomain is less than 3 characters', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
);
fireEvent.click(screen.getByRole('button', { name: /edit workspace link/i }));
const input = screen.getByRole('textbox');
await user.clear(input);
await user.type(input, 'ab');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
fireEvent.change(input, { target: { value: 'ab' } });
fireEvent.click(screen.getByRole('button', { name: /apply changes/i }));
expect(
screen.getByText(/minimum 3 characters required/i),
@@ -174,19 +142,12 @@ describe('CustomDomainSettings', () => {
});
it('shows all workspace URLs as links in the dropdown', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
// Open the URL dropdown
await user.click(
fireEvent.click(
screen.getByRole('button', { name: /custom-host\.test\.cloud/i }),
);
@@ -207,26 +168,19 @@ describe('CustomDomainSettings', () => {
it('calls toast.custom with new URL after successful domain update', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
rest.put(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({})),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
);
fireEvent.click(screen.getByRole('button', { name: /edit workspace link/i }));
const input = screen.getByRole('textbox');
await user.clear(input);
await user.type(input, 'myteam');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
fireEvent.change(input, { target: { value: 'myteam' } });
fireEvent.click(screen.getByRole('button', { name: /apply changes/i }));
// Verify toast.custom was called
await waitFor(() => {
@@ -243,12 +197,6 @@ describe('CustomDomainSettings', () => {
describe('Workspace Name rendering', () => {
it('renders org displayName when available from appContext', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
render(<CustomDomainSettings />, undefined, {
appContextOverrides: {
org: [{ id: 'xyz', displayName: 'My Org Name', createdAt: 0 }],
@@ -259,12 +207,6 @@ describe('CustomDomainSettings', () => {
});
it('falls back to customDomainSubdomain when org displayName is missing', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
render(<CustomDomainSettings />, undefined, {
appContextOverrides: { org: [] },
});

View File

@@ -1,12 +1,6 @@
import setRetentionApiV2 from 'api/settings/setRetentionV2';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import {
fireEvent,
render,
screen,
userEvent,
waitFor,
} from 'tests/test-utils';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
import { IDiskType } from 'types/api/disks/getDisks';
import {
PayloadPropsLogs,
@@ -115,8 +109,6 @@ describe('GeneralSettings - S3 Logs Retention', () => {
describe('Test 1: S3 Enabled - Only Days in Dropdown', () => {
it('should show only Days option for S3 retention and send correct API payload', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<GeneralSettings
metricsTtlValuesPayload={mockMetricsRetention}
@@ -159,8 +151,7 @@ describe('GeneralSettings - S3 Logs Retention', () => {
fireEvent.click(document.body);
// Change S3 retention value to 5 days
await user.clear(s3Input);
await user.type(s3Input, '5');
fireEvent.change(s3Input, { target: { value: '5' } });
// Find the save button in the Logs row
const saveButton = logsRow.querySelector(
@@ -217,8 +208,6 @@ describe('GeneralSettings - S3 Logs Retention', () => {
describe('Test 2: S3 Disabled - Field Hidden', () => {
it('should hide S3 retention field and send empty S3 values to API', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<GeneralSettings
metricsTtlValuesPayload={mockMetricsRetention}
@@ -245,7 +234,7 @@ describe('GeneralSettings - S3 Logs Retention', () => {
const totalDropdown = logsRow.querySelector(
'.ant-select-selector',
) as HTMLElement;
await user.click(totalDropdown);
fireEvent.mouseDown(totalDropdown);
// Wait for dropdown options to appear
await waitFor(() => {
@@ -259,11 +248,10 @@ describe('GeneralSettings - S3 Logs Retention', () => {
opt.textContent?.includes('Days'),
);
expect(daysOption).toBeInTheDocument();
await user.click(daysOption as HTMLElement);
fireEvent.click(daysOption as HTMLElement);
// Now change the value
await user.clear(totalInput);
await user.type(totalInput, '60');
fireEvent.change(totalInput, { target: { value: '60' } });
// Find the save button
const saveButton = logsRow.querySelector(
@@ -277,14 +265,14 @@ describe('GeneralSettings - S3 Logs Retention', () => {
});
// Click save button
await user.click(saveButton);
fireEvent.click(saveButton);
// Wait for modal to appear
const okButton = await screen.findByRole('button', { name: /ok/i });
expect(okButton).toBeInTheDocument();
// Click OK button
await user.click(okButton);
fireEvent.click(okButton);
// Verify API was called with empty S3 values (60 days)
await waitFor(() => {
@@ -333,8 +321,6 @@ describe('GeneralSettings - S3 Logs Retention', () => {
describe('Test 4: Save Button State with S3 Disabled', () => {
it('should disable save button when cold_storage_ttl_days is -1 and no changes made', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<GeneralSettings
metricsTtlValuesPayload={mockMetricsRetention}
@@ -365,8 +351,7 @@ describe('GeneralSettings - S3 Logs Retention', () => {
const totalInput = inputs[0] as HTMLInputElement;
// Change total retention value to trigger button enable
await user.clear(totalInput);
await user.type(totalInput, '60');
fireEvent.change(totalInput, { target: { value: '60' } });
// Button should now be enabled after change
await waitFor(() => {
@@ -374,8 +359,7 @@ describe('GeneralSettings - S3 Logs Retention', () => {
});
// Revert to original value (30 days displays as 1 Month)
await user.clear(totalInput);
await user.type(totalInput, '1');
fireEvent.change(totalInput, { target: { value: '1' } });
// Button should be disabled again (back to original state)
await waitFor(() => {

View File

@@ -1,6 +1,8 @@
import { Dispatch, SetStateAction, useState } from 'react';
import { useQueryClient } from 'react-query';
import { patchRulePartial } from 'api/alerts/patchRulePartial';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { invalidateGetRuleByID } from 'api/generated/services/rules';
import type {
RenderErrorResponseDTO,
RuletypesRuleDTO,
@@ -28,6 +30,7 @@ function ToggleAlertState({
const { notifications } = useNotifications();
const { showErrorModal } = useErrorModal();
const queryClient = useQueryClient();
const onToggleHandler = async (
id: string,
@@ -60,6 +63,9 @@ function ToggleAlertState({
loading: false,
payload: updatedRule,
}));
invalidateGetRuleByID(queryClient, { id });
notifications.success({
message: 'Success',
});

View File

@@ -1,6 +1,6 @@
import type { TypesUserDTO } from 'api/generated/services/sigNoz.schemas';
import { rest, server } from 'mocks-server/server';
import { render, screen, userEvent } from 'tests/test-utils';
import { fireEvent, render, screen } from 'tests/test-utils';
import MembersSettings from '../MembersSettings';
@@ -76,32 +76,27 @@ 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');
await user.click(screen.getByRole('button', { name: /all members/i }));
fireEvent.click(screen.getByRole('button', { name: /all members/i }));
const pendingOption = await screen.findByText(/pending invites/i);
await user.click(pendingOption);
fireEvent.click(pendingOption);
await screen.findByText('charlie@signoz.io');
expect(screen.queryByText('Alice Smith')).not.toBeInTheDocument();
});
it('filters members by name using the search input', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<MembersSettings />);
await screen.findByText('Alice Smith');
await user.type(
screen.getByPlaceholderText(/Search by name or email/i),
'bob',
);
fireEvent.change(screen.getByPlaceholderText(/Search by name or email/i), {
target: { value: 'bob' },
});
await screen.findByText('Bob Jones');
expect(screen.queryByText('Alice Smith')).not.toBeInTheDocument();
@@ -109,31 +104,25 @@ describe('MembersSettings (integration)', () => {
});
it('opens EditMemberDrawer when an active member row is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<MembersSettings />);
await user.click(await screen.findByText('Alice Smith'));
fireEvent.click(await screen.findByText('Alice Smith'));
await screen.findByText('Member Details');
});
it('opens EditMemberDrawer when a deleted member row is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<MembersSettings />);
await user.click(await screen.findByText('Dave Deleted'));
fireEvent.click(await screen.findByText('Dave Deleted'));
expect(screen.queryByText('Member Details')).toBeInTheDocument();
});
it('opens InviteMembersModal when "Invite member" button is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<MembersSettings />);
await user.click(screen.getByRole('button', { name: /invite member/i }));
fireEvent.click(screen.getByRole('button', { name: /invite member/i }));
await expect(
screen.findAllByPlaceholderText('john@signoz.io'),

View File

@@ -117,8 +117,7 @@ describe('CreateEdit Modal', () => {
});
});
// Todo: to fixed properly - failing with - due to timeout > 5000ms
describe.skip('Form Validation', () => {
describe('Form Validation', () => {
it('shows validation error when submitting without required fields', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
@@ -127,7 +126,7 @@ describe('CreateEdit Modal', () => {
const configureButtons = await screen.findAllByRole('button', {
name: /configure/i,
});
await user.click(configureButtons[0]);
fireEvent.click(configureButtons[0]);
const saveButton = await screen.findByRole('button', {
name: /save changes/i,
@@ -338,11 +337,8 @@ describe('CreateEdit Modal', () => {
});
});
// Todo: to fixed properly - failing with - due to timeout > 5000ms
describe.skip('Modal Actions', () => {
it('calls onClose when cancel button is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
describe('Modal Actions', () => {
it('calls onClose when cancel button is clicked', () => {
render(
<CreateEdit
isCreate={false}
@@ -352,7 +348,7 @@ describe('CreateEdit Modal', () => {
);
const cancelButton = screen.getByRole('button', { name: /cancel/i });
await user.click(cancelButton);
fireEvent.click(cancelButton);
expect(mockOnClose).toHaveBeenCalled();
});

View File

@@ -1,13 +1,20 @@
// Ungate feature flag for all tests in this file
jest.mock('../../config', () => ({ IS_ROLE_DETAILS_AND_CRUD_ENABLED: true }));
import * as roleApi from 'api/generated/services/role';
import {
customRoleResponse,
managedRoleResponse,
} from 'mocks-server/__mockdata__/roles';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
import {
fireEvent,
render,
screen,
userEvent,
waitFor,
within,
} from 'tests/test-utils';
import RoleDetailsPage from '../RoleDetailsPage';
@@ -22,7 +29,7 @@ const allScopeObjectsResponse = {
status: 'success',
data: [
{
resource: { name: 'dashboard', type: 'dashboard' },
resource: { kind: 'role', type: 'metaresources' },
selectors: ['*'],
},
],
@@ -44,8 +51,7 @@ afterEach(() => {
server.resetHandlers();
});
// Todo: to fixed properly - failing with - due to timeout > 5000ms
describe.skip('RoleDetailsPage', () => {
describe('RoleDetailsPage', () => {
it('renders custom role header, tabs, description, permissions, and action buttons', async () => {
setupDefaultHandlers();
@@ -57,20 +63,16 @@ describe.skip('RoleDetailsPage', () => {
screen.findByText('Role — billing-manager'),
).resolves.toBeInTheDocument();
// Tab navigation
expect(screen.getByText('Overview')).toBeInTheDocument();
expect(screen.getByText('Members')).toBeInTheDocument();
// Role description (OverviewTab)
expect(
screen.getByText('Custom role for managing billing and invoices.'),
).toBeInTheDocument();
// Permission items derived from mocked authz relations
expect(screen.getByText('Create')).toBeInTheDocument();
expect(screen.getByText('Read')).toBeInTheDocument();
// Action buttons present for custom role
expect(
screen.getByRole('button', { name: /edit role details/i }),
).toBeInTheDocument();
@@ -96,14 +98,13 @@ describe.skip('RoleDetailsPage', () => {
),
).toBeInTheDocument();
// Action buttons absent for managed role
expect(screen.queryByText('Edit Role Details')).not.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: /delete role/i }),
).not.toBeInTheDocument();
});
it('edit flow: modal opens pre-filled and calls PATCH on save and verify', async () => {
it('edit flow: modal opens pre-filled and calls PATCH on save', async () => {
const patchSpy = jest.fn();
let description = customRoleResponse.data.description;
server.use(
@@ -138,21 +139,16 @@ describe.skip('RoleDetailsPage', () => {
await screen.findByText('Role — billing-manager');
// Open the edit modal
await user.click(screen.getByRole('button', { name: /edit role details/i }));
await expect(
screen.findByText('Edit Role Details', {
selector: '.ant-modal-title',
}),
screen.findByText('Edit Role Details', { selector: '.ant-modal-title' }),
).resolves.toBeInTheDocument();
// Name field is disabled in edit mode (role rename is not allowed)
const nameInput = screen.getByPlaceholderText(
'Enter role name e.g. : Service Owner',
);
expect(nameInput).toBeDisabled();
// Update description and save
const descField = screen.getByPlaceholderText(
'A helpful description of the role',
);
@@ -168,9 +164,7 @@ describe.skip('RoleDetailsPage', () => {
await waitFor(() =>
expect(
screen.queryByText('Edit Role Details', {
selector: '.ant-modal-title',
}),
screen.queryByText('Edit Role Details', { selector: '.ant-modal-title' }),
).not.toBeInTheDocument(),
);
@@ -219,58 +213,61 @@ describe.skip('RoleDetailsPage', () => {
});
describe('permission side panel', () => {
async function openCreatePanel(
user: ReturnType<typeof userEvent.setup>,
): Promise<void> {
beforeEach(() => {
// Both hooks mocked so data renders synchronously — no React Query scheduler or MSW round-trip.
jest.spyOn(roleApi, 'useGetRole').mockReturnValue({
data: customRoleResponse,
isLoading: false,
isFetching: false,
isError: false,
error: null,
} as any);
jest
.spyOn(roleApi, 'useGetObjects')
.mockReturnValue({ data: emptyObjectsResponse, isLoading: false } as any);
});
afterEach(() => {
jest.restoreAllMocks();
});
async function openCreatePanel(): Promise<HTMLElement> {
await screen.findByText('Role — billing-manager');
await user.click(screen.getByText('Create'));
fireEvent.click(screen.getByText('Create'));
await screen.findByText('Edit Create Permissions');
await screen.findByRole('button', { name: /dashboard/i });
const panel = document.querySelector(
'.permission-side-panel',
) as HTMLElement;
await within(panel).findByRole('button', { name: 'Role' });
return panel;
}
it('Save Changes is disabled until a resource scope is changed', async () => {
setupDefaultHandlers();
server.use(
rest.get(
`${rolesApiBase}/:id/relation/:relation/objects`,
(_req, res, ctx) => res(ctx.status(200), ctx.json(emptyObjectsResponse)),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<RoleDetailsPage />, undefined, {
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
});
await openCreatePanel(user);
// No change yet — config matches initial, unsavedCount = 0
expect(screen.getByRole('button', { name: /save changes/i })).toBeDisabled();
// Expand Dashboard and flip to All — now Save is enabled
await user.click(screen.getByRole('button', { name: /dashboard/i }));
await user.click(screen.getByText('All'));
const panel = await openCreatePanel();
expect(
screen.getByRole('button', { name: /save changes/i }),
).not.toBeDisabled();
within(panel).getByRole('button', { name: /save changes/i }),
).toBeDisabled();
// check for what shown now - unsavedCount = 1
fireEvent.click(within(panel).getByRole('button', { name: 'Role' }));
fireEvent.click(screen.getByText('All'));
expect(
within(panel).getByRole('button', { name: /save changes/i }),
).not.toBeDisabled();
expect(screen.getByText('1 unsaved change')).toBeInTheDocument();
});
it('set scope to All → patchObjects additions: ["*"], deletions: null', async () => {
const patchSpy = jest.fn();
setupDefaultHandlers();
server.use(
rest.get(
`${rolesApiBase}/:id/relation/:relation/objects`,
(_req, res, ctx) => res(ctx.status(200), ctx.json(emptyObjectsResponse)),
),
rest.patch(
`${rolesApiBase}/:id/relation/:relation/objects`,
`${rolesApiBase}/:id/relations/:relation/objects`,
async (req, res, ctx) => {
patchSpy(await req.json());
return res(ctx.status(200), ctx.json({ status: 'success', data: null }));
@@ -278,23 +275,23 @@ describe.skip('RoleDetailsPage', () => {
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<RoleDetailsPage />, undefined, {
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
});
await openCreatePanel(user);
const panel = await openCreatePanel();
await user.click(screen.getByRole('button', { name: /dashboard/i }));
await user.click(screen.getByText('All'));
await user.click(screen.getByRole('button', { name: /save changes/i }));
fireEvent.click(within(panel).getByRole('button', { name: 'Role' }));
fireEvent.click(screen.getByText('All'));
fireEvent.click(
within(panel).getByRole('button', { name: /save changes/i }),
);
await waitFor(() =>
expect(patchSpy).toHaveBeenCalledWith({
additions: [
{
resource: { name: 'dashboard', type: 'dashboard' },
resource: { kind: 'role', type: 'metaresources' },
selectors: ['*'],
},
],
@@ -306,14 +303,9 @@ describe.skip('RoleDetailsPage', () => {
it('set scope to Only selected with IDs → patchObjects additions contain those IDs', async () => {
const patchSpy = jest.fn();
setupDefaultHandlers();
server.use(
rest.get(
`${rolesApiBase}/:id/relation/:relation/objects`,
(_req, res, ctx) => res(ctx.status(200), ctx.json(emptyObjectsResponse)),
),
rest.patch(
`${rolesApiBase}/:id/relation/:relation/objects`,
`${rolesApiBase}/:id/relations/:relation/objects`,
async (req, res, ctx) => {
patchSpy(await req.json());
return res(ctx.status(200), ctx.json({ status: 'success', data: null }));
@@ -327,23 +319,25 @@ describe.skip('RoleDetailsPage', () => {
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
});
await openCreatePanel(user);
const panel = await openCreatePanel();
await user.click(screen.getByRole('button', { name: /dashboard/i }));
fireEvent.click(within(panel).getByRole('button', { name: 'Role' }));
const combobox = screen.getByRole('combobox');
await user.click(combobox);
await user.type(combobox, 'dash-1');
const combobox = within(panel).getByRole('combobox');
await user.type(combobox, 'role-001');
await user.keyboard('{Enter}');
await user.click(screen.getByRole('button', { name: /save changes/i }));
// user.click waits for the tag-addition state to settle before clicking.
await user.click(
within(panel).getByRole('button', { name: /save changes/i }),
);
await waitFor(() =>
expect(patchSpy).toHaveBeenCalledWith({
additions: [
{
resource: { name: 'dashboard', type: 'dashboard' },
selectors: ['dash-1'],
resource: { kind: 'role', type: 'metaresources' },
selectors: ['role-001'],
},
],
deletions: null,
@@ -354,15 +348,13 @@ describe.skip('RoleDetailsPage', () => {
it('existing All scope changed to Only selected (empty) → patchObjects deletions: ["*"], additions: null', async () => {
const patchSpy = jest.fn();
setupDefaultHandlers();
jest.spyOn(roleApi, 'useGetObjects').mockReturnValue({
data: allScopeObjectsResponse,
isLoading: false,
} as any);
server.use(
rest.get(
`${rolesApiBase}/:id/relation/:relation/objects`,
(_req, res, ctx) =>
res(ctx.status(200), ctx.json(allScopeObjectsResponse)),
),
rest.patch(
`${rolesApiBase}/:id/relation/:relation/objects`,
`${rolesApiBase}/:id/relations/:relation/objects`,
async (req, res, ctx) => {
patchSpy(await req.json());
return res(ctx.status(200), ctx.json({ status: 'success', data: null }));
@@ -370,26 +362,24 @@ describe.skip('RoleDetailsPage', () => {
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<RoleDetailsPage />, undefined, {
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
});
await openCreatePanel(user);
const panel = await openCreatePanel();
await user.click(screen.getByRole('button', { name: /dashboard/i }));
fireEvent.click(within(panel).getByRole('button', { name: 'Role' }));
fireEvent.click(screen.getByText('Only selected'));
fireEvent.click(
within(panel).getByRole('button', { name: /save changes/i }),
);
await user.click(screen.getByText('Only selected'));
await user.click(screen.getByRole('button', { name: /save changes/i }));
// Should delete the '*' selector and add nothing
await waitFor(() =>
expect(patchSpy).toHaveBeenCalledWith({
additions: null,
deletions: [
{
resource: { name: 'dashboard', type: 'dashboard' },
resource: { kind: 'role', type: 'metaresources' },
selectors: ['*'],
},
],
@@ -398,36 +388,25 @@ describe.skip('RoleDetailsPage', () => {
});
it('unsaved changes counter shown on scope change, Discard resets it', async () => {
setupDefaultHandlers();
server.use(
rest.get(
`${rolesApiBase}/:id/relation/:relation/objects`,
(_req, res, ctx) => res(ctx.status(200), ctx.json(emptyObjectsResponse)),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<RoleDetailsPage />, undefined, {
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
});
await openCreatePanel(user);
const panel = await openCreatePanel();
// No unsaved changes indicator yet
expect(screen.queryByText(/unsaved change/)).not.toBeInTheDocument();
// Change dashboard scope to "All"
await user.click(screen.getByRole('button', { name: /dashboard/i }));
await user.click(screen.getByText('All'));
fireEvent.click(within(panel).getByRole('button', { name: 'Role' }));
fireEvent.click(screen.getByText('All'));
expect(screen.getByText('1 unsaved change')).toBeInTheDocument();
// Discard reverts to initial config — counter disappears, Save re-disabled
await user.click(screen.getByRole('button', { name: /discard/i }));
fireEvent.click(within(panel).getByRole('button', { name: /discard/i }));
expect(screen.queryByText(/unsaved change/)).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: /save changes/i })).toBeDisabled();
expect(
within(panel).getByRole('button', { name: /save changes/i }),
).toBeDisabled();
});
});
});

View File

@@ -4,24 +4,27 @@ import {
} from 'mocks-server/__mockdata__/roles';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, userEvent } from 'tests/test-utils';
import { fireEvent, render, screen } from 'tests/test-utils';
import RolesSettings from '../RolesSettings';
const rolesApiURL = 'http://localhost/api/v1/roles';
describe('RolesSettings', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('renders the header and search input', () => {
beforeEach(() => {
server.use(
rest.get(rolesApiURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
);
});
afterEach(() => {
jest.clearAllMocks();
server.resetHandlers();
});
it('renders the header and search input', () => {
render(<RolesSettings />);
expect(screen.getByText('Roles')).toBeInTheDocument();
@@ -34,12 +37,6 @@ describe('RolesSettings', () => {
});
it('displays roles grouped by managed and custom sections', async () => {
server.use(
rest.get(rolesApiURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
);
render(<RolesSettings />);
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();
@@ -68,20 +65,13 @@ describe('RolesSettings', () => {
});
it('filters roles by search query on name', async () => {
server.use(
rest.get(rolesApiURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
);
render(<RolesSettings />);
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const searchInput = screen.getByPlaceholderText('Search for roles...');
await user.type(searchInput, 'billing');
fireEvent.change(screen.getByPlaceholderText('Search for roles...'), {
target: { value: 'billing' },
});
await expect(
screen.findByText('billing-manager'),
@@ -92,20 +82,13 @@ describe('RolesSettings', () => {
});
it('filters roles by search query on description', async () => {
server.use(
rest.get(rolesApiURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
);
render(<RolesSettings />);
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const searchInput = screen.getByPlaceholderText('Search for roles...');
await user.type(searchInput, 'read-only');
fireEvent.change(screen.getByPlaceholderText('Search for roles...'), {
target: { value: 'read-only' },
});
await expect(screen.findByText('signoz-viewer')).resolves.toBeInTheDocument();
expect(screen.queryByText('signoz-admin')).not.toBeInTheDocument();
@@ -113,20 +96,13 @@ describe('RolesSettings', () => {
});
it('shows empty state when search matches nothing', async () => {
server.use(
rest.get(rolesApiURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
);
render(<RolesSettings />);
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const searchInput = screen.getByPlaceholderText('Search for roles...');
await user.type(searchInput, 'nonexistentrole');
fireEvent.change(screen.getByPlaceholderText('Search for roles...'), {
target: { value: 'nonexistentrole' },
});
await expect(
screen.findByText('No roles match your search.'),
@@ -183,12 +159,6 @@ describe('RolesSettings', () => {
});
it('renders descriptions for all roles', async () => {
server.use(
rest.get(rolesApiURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
);
render(<RolesSettings />);
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();

View File

@@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
import ServiceAccountsSettings from '../ServiceAccountsSettings';
@@ -123,8 +123,6 @@ describe('ServiceAccountsSettings (integration)', () => {
});
it('filter dropdown to "Active" hides DISABLED accounts', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<NuqsTestingAdapter>
<ServiceAccountsSettings />
@@ -133,18 +131,16 @@ describe('ServiceAccountsSettings (integration)', () => {
await screen.findByText('CI Bot');
await user.click(screen.getByRole('button', { name: /All accounts/i }));
fireEvent.click(screen.getByRole('button', { name: /All accounts/i }));
const activeOption = await screen.findByText(/Active ⎯/i);
await user.click(activeOption);
fireEvent.click(activeOption);
await screen.findByText('CI Bot');
expect(screen.queryByText('Legacy Bot')).not.toBeInTheDocument();
});
it('search by name filters accounts in real-time', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<NuqsTestingAdapter>
<ServiceAccountsSettings />
@@ -153,10 +149,9 @@ describe('ServiceAccountsSettings (integration)', () => {
await screen.findByText('CI Bot');
await user.type(
screen.getByPlaceholderText(/Search by name or email/i),
'legacy',
);
fireEvent.change(screen.getByPlaceholderText(/Search by name or email/i), {
target: { value: 'legacy' },
});
await screen.findByText('Legacy Bot');
expect(screen.queryByText('CI Bot')).not.toBeInTheDocument();
@@ -164,15 +159,13 @@ describe('ServiceAccountsSettings (integration)', () => {
});
it('clicking a row opens the drawer with account details visible', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<NuqsTestingAdapter hasMemory>
<ServiceAccountsSettings />
</NuqsTestingAdapter>,
);
await user.click(
fireEvent.click(
await screen.findByRole('button', {
name: /View service account CI Bot/i,
}),
@@ -184,7 +177,6 @@ describe('ServiceAccountsSettings (integration)', () => {
});
it('saving changes in the drawer refetches the list', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const listRefetchSpy = jest.fn();
server.use(
@@ -206,15 +198,14 @@ describe('ServiceAccountsSettings (integration)', () => {
await screen.findByText('CI Bot');
listRefetchSpy.mockClear();
await user.click(
fireEvent.click(
await screen.findByRole('button', { name: /View service account CI Bot/i }),
);
const nameInput = await screen.findByDisplayValue('CI Bot');
await user.clear(nameInput);
await user.type(nameInput, 'CI Bot Updated');
fireEvent.change(nameInput, { target: { value: 'CI Bot Updated' } });
await user.click(screen.getByRole('button', { name: /Save Changes/i }));
fireEvent.click(screen.getByRole('button', { name: /Save Changes/i }));
await screen.findByDisplayValue('CI Bot Updated');
await waitFor(() => {
@@ -223,8 +214,6 @@ describe('ServiceAccountsSettings (integration)', () => {
});
it('"New Service Account" button opens the Create Service Account modal', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<NuqsTestingAdapter hasMemory>
<ServiceAccountsSettings />
@@ -233,9 +222,7 @@ describe('ServiceAccountsSettings (integration)', () => {
await screen.findByText('CI Bot');
await user.click(
screen.getByRole('button', { name: /New Service Account/i }),
);
fireEvent.click(screen.getByRole('button', { name: /New Service Account/i }));
await screen.findByRole('dialog', { name: /New Service Account/i });
expect(screen.getByPlaceholderText('Enter a name')).toBeInTheDocument();

View File

@@ -17,7 +17,7 @@ export default function AlertState({
let label;
const isDarkMode = useIsDarkMode();
switch (state) {
case 'no-data':
case 'nodata':
icon = (
<CircleOff
size={18}

View File

@@ -12,6 +12,7 @@ import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
createRule,
deleteRuleByID,
invalidateGetRuleByID,
updateRuleByID,
useGetRuleByID,
useListRules,
@@ -408,6 +409,7 @@ export const useAlertRuleStatusToggle = ({
{
onSuccess: (data) => {
setAlertRuleState(data.data.state);
invalidateGetRuleByID(queryClient, { id: ruleId });
queryClient.refetchQueries([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId]);
notifications.success({
message: `Alert has been ${
@@ -416,6 +418,7 @@ export const useAlertRuleStatusToggle = ({
});
},
onError: (error) => {
invalidateGetRuleByID(queryClient, { id: ruleId });
queryClient.refetchQueries([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId]);
showErrorModal(
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,

View File

@@ -109,7 +109,8 @@ export type AlertRuleTimelineTableResponsePayload = {
labels: AlertLabelsProps['labels'];
};
};
type AlertState = 'firing' | 'normal' | 'no-data' | 'muted';
type AlertState = 'firing' | 'normal' | 'nodata' | 'muted';
export interface AlertRuleTimelineGraphResponse {
start: number;

View File

@@ -134,7 +134,7 @@ type RuleStateHistory struct {
// One of ["normal", "firing"]
OverallState AlertState `json:"overallState" ch:"overall_state"`
OverallStateChanged bool `json:"overallStateChanged" ch:"overall_state_changed"`
// One of ["normal", "firing", "no_data", "muted"]
// One of ["normal", "firing", "nodata", "muted"]
State AlertState `json:"state" ch:"state"`
StateChanged bool `json:"stateChanged" ch:"state_changed"`
UnixMilli int64 `json:"unixMilli" ch:"unix_milli"`

View File

@@ -69,7 +69,7 @@ class BuilderQuery:
class TraceOperatorQuery:
name: str
expression: str
return_spans_from: str
return_spans_from: str | None = None
limit: int | None = None
order: list[OrderBy] | None = None
@@ -77,8 +77,9 @@ class TraceOperatorQuery:
spec: dict[str, Any] = {
"name": self.name,
"expression": self.expression,
"returnSpansFrom": self.return_spans_from,
}
if self.return_spans_from is not None:
spec["returnSpansFrom"] = self.return_spans_from
if self.limit is not None:
spec["limit"] = self.limit
if self.order:

View File

@@ -625,7 +625,6 @@ def test_export_traces_with_composite_query_trace_operator(
query_c = TraceOperatorQuery(
name="C",
expression="A => B",
return_spans_from="A",
limit=1000,
order=[OrderBy(TelemetryFieldKey("timestamp", "string", "span"), "desc")],
)
@@ -652,17 +651,15 @@ def test_export_traces_with_composite_query_trace_operator(
# Parse JSONL content
jsonl_lines = response.text.strip().split("\n")
assert len(jsonl_lines) == 1, f"Expected at least 1 line, got {len(jsonl_lines)}"
assert len(jsonl_lines) >= 1, f"Expected at least 1 line, got {len(jsonl_lines)}"
# Verify all returned spans belong to the matched trace
# Verify all returned spans belong to the matched trace.
# The direct-descendant JOIN emits one row per matching child, so the parent
# span may appear more than once (once per child that satisfies the condition).
json_objects = [json.loads(line) for line in jsonl_lines]
trace_ids = [obj.get("trace_id") for obj in json_objects]
assert all(tid == parent_trace_id for tid in trace_ids)
# Verify the parent span (returnSpansFrom = "A") is present
span_names = [obj.get("name") for obj in json_objects]
assert "parent-operation" in span_names
def test_export_traces_with_select_fields(
signoz: types.SigNoz,