Compare commits

...

7 Commits

Author SHA1 Message Date
SagarRajput-7
6c19ce0854 feat(ingestion): updated test cases 2026-05-07 05:37:55 +05:30
SagarRajput-7
11212100be feat(ingestion): code refactor 2026-05-07 05:06:59 +05:30
SagarRajput-7
fb91440f56 feat(ingestion): added test cases 2026-05-07 04:52:04 +05:30
SagarRajput-7
6087c3aa09 Merge branch 'main' into ingestion-alert-option 2026-05-07 04:43:06 +05:30
SagarRajput-7
4c191b60fa feat(ingestion): removed antd button, used signozhq library 2026-05-07 04:38:12 +05:30
SagarRajput-7
9106930cbd feat(ingestion): added helper text and set alert badge for ingestion 2026-05-07 04:05:09 +05:30
SagarRajput-7
f6bc255f4d feat(ingestion): added default alert name when coming from ingestion limit 2026-05-07 00:55:35 +05:30
5 changed files with 238 additions and 100 deletions

View File

@@ -56,4 +56,5 @@ export enum QueryParams {
showClassicCreateAlertsPage = 'showClassicCreateAlertsPage',
isTestAlert = 'isTestAlert',
yAxisUnit = 'yAxisUnit',
ruleName = 'ruleName',
}

View File

@@ -7,6 +7,7 @@ import {
useEffect,
useMemo,
useReducer,
useRef,
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
@@ -123,6 +124,7 @@ export function CreateAlertProvider(
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const thresholdsFromURL = queryParams.get(QueryParams.thresholds);
const ruleNameFromURL = queryParams.get(QueryParams.ruleName);
const [alertType, setAlertType] = useState<AlertTypes>(() => {
if (isEditMode) {
@@ -154,6 +156,8 @@ export function CreateAlertProvider(
[redirectWithQueryBuilderData],
);
const ruleNameAppliedRef = useRef(false);
useEffect(() => {
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
@@ -191,7 +195,18 @@ export function CreateAlertProvider(
},
});
}
}, [alertType, thresholdsFromURL]);
if (ruleNameFromURL && !ruleNameAppliedRef.current) {
ruleNameAppliedRef.current = true;
setCreateAlertState({
slice: CreateAlertSlice.BASIC,
action: {
type: 'SET_ALERT_NAME',
payload: ruleNameFromURL,
},
});
}
}, [alertType, thresholdsFromURL, ruleNameFromURL]);
useEffect(() => {
if (isEditMode && initialAlertState) {

View File

@@ -443,7 +443,25 @@
.signal-limit-save-discard {
display: flex;
gap: 8px;
gap: var(--spacing-4);
.signal-limit-save-discard-actions {
display: flex;
align-items: center;
gap: var(--spacing-4);
}
.signal-limit-alert-helper {
display: flex;
align-items: center;
gap: var(--spacing-2);
font-size: var(--paragraph-small-400-font-size);
color: var(--l2-foreground);
border-bottom: 1px dashed var(--l2-foreground);
padding-bottom: 1px;
font-style: italic;
margin-left: var(--spacing-6);
}
}
}
}
@@ -475,6 +493,7 @@
.ant-modal-footer {
padding: 16px;
margin-top: 0;
gap: 8px;
display: flex;
justify-content: flex-end;

View File

@@ -3,8 +3,8 @@ import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import { Badge, Button } from '@signozhq/ui';
import {
Button,
Col,
Collapse,
DatePicker,
@@ -394,7 +394,7 @@ function MultiIngestionSettings(): JSX.Element {
notifications.success({
message: 'Ingestion key deleted successfully',
});
refetchAPIKeys();
void refetchAPIKeys();
setIsDeleteModalOpen(false);
},
onError: (error) => {
@@ -426,7 +426,7 @@ function MultiIngestionSettings(): JSX.Element {
notifications.success({
message: 'Ingestion key updated successfully',
});
refetchAPIKeys();
void refetchAPIKeys();
setIsEditModalOpen(false);
},
onError: (error) => {
@@ -466,7 +466,7 @@ function MultiIngestionSettings(): JSX.Element {
setActiveAPIKey(null);
setUpdatedTags([]);
hideAddViewModal();
refetchAPIKeys();
void refetchAPIKeys();
},
onError: (error) => {
showErrorNotification(notifications, error as AxiosError);
@@ -630,13 +630,14 @@ function MultiIngestionSettings(): JSX.Element {
onSuccess: () => {
notifications.success({
message: 'Limit created successfully',
description: "Set up an alert to know when you're close to hitting it.",
});
setActiveSignal(null);
setActiveAPIKey(null);
setIsEditAddLimitOpen(false);
setUpdatedTags([]);
hideAddViewModal();
refetchAPIKeys();
void refetchAPIKeys();
setHasCreateLimitForIngestionKeyError(false);
},
onError: (error: AxiosError<RenderErrorResponseDTO>) => {
@@ -733,13 +734,14 @@ function MultiIngestionSettings(): JSX.Element {
onSuccess: () => {
notifications.success({
message: 'Limit updated successfully',
description: "Set up an alert to know when you're close to hitting it.",
});
setActiveSignal(null);
setActiveAPIKey(null);
setIsEditAddLimitOpen(false);
setUpdatedTags([]);
hideAddViewModal();
refetchAPIKeys();
void refetchAPIKeys();
setHasUpdateLimitForIngestionKeyError(false);
},
onError: (error: AxiosError<RenderErrorResponseDTO>) => {
@@ -824,7 +826,7 @@ function MultiIngestionSettings(): JSX.Element {
});
setIsDeleteModalOpen(false);
setIsDeleteLimitModalOpen(false);
refetchAPIKeys();
void refetchAPIKeys();
},
onError: (error) => {
showErrorNotification(notifications, error as AxiosError);
@@ -889,11 +891,18 @@ function MultiIngestionSettings(): JSX.Element {
const thresholds = cloneDeep(INITIAL_ALERT_THRESHOLD_STATE.thresholds);
thresholds[0].thresholdValue = threshold;
const keyName = APIKey.name?.trim();
const ruleName = keyName
? `${keyName} - daily ${signal.signal} ingestion limit`
: `Daily ${signal.signal} ingestion limit`;
const URL = `${ROUTES.ALERTS_NEW}?${
QueryParams.compositeQuery
}=${encodeURIComponent(stringifiedQuery)}&${
QueryParams.thresholds
}=${encodeURIComponent(JSON.stringify(thresholds))}`;
}=${encodeURIComponent(JSON.stringify(thresholds))}&${
QueryParams.ruleName
}=${encodeURIComponent(ruleName)}`;
history.push(URL);
};
@@ -980,13 +989,18 @@ function MultiIngestionSettings(): JSX.Element {
</div>
<div className="action-btn">
<Button
className="periscope-btn ghost"
icon={<PenLine size={14} />}
variant="link"
size="icon"
color="secondary"
suffix={<PenLine size={14} />}
aria-label="Edit ingestion key"
onClick={onEditKey}
/>
<Button
className="periscope-btn ghost"
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
variant="link"
size="icon"
color="destructive"
suffix={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
onClick={onDeleteKey}
/>
</div>
@@ -1092,16 +1106,22 @@ function MultiIngestionSettings(): JSX.Element {
{hasLimits(signalName) ? (
<>
<Button
className="periscope-btn ghost"
icon={<PenLine size={14} />}
variant="link"
size="icon"
color="secondary"
prefix={<PenLine size={14} />}
aria-label={`Edit ${signalName} limit`}
disabled={
!!(activeAPIKey?.id === APIKey?.id && activeSignal)
}
onClick={onEditSignalLimit}
/>
<Button
className="periscope-btn ghost"
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
variant="link"
size="icon"
color="destructive"
prefix={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
aria-label={`Delete ${signalName} limit`}
disabled={
!!(activeAPIKey?.id === APIKey?.id && activeSignal)
}
@@ -1110,10 +1130,10 @@ function MultiIngestionSettings(): JSX.Element {
</>
) : (
<Button
className="periscope-btn"
size="small"
shape="round"
icon={<PlusIcon size={14} />}
variant="link"
size="sm"
color="secondary"
prefix={<PlusIcon size={14} />}
disabled={!!(activeAPIKey?.id === APIKey?.id && activeSignal)}
onClick={onAddSignalLimit}
>
@@ -1344,31 +1364,35 @@ function MultiIngestionSettings(): JSX.Element {
activeSignal.signal === signalName &&
isEditAddLimitOpen && (
<div className="signal-limit-save-discard">
<Button
type="primary"
className="periscope-btn primary"
size="small"
disabled={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
loading={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
onClick={onSaveSignalLimit}
>
Save
</Button>
<Button
type="default"
className="periscope-btn"
size="small"
disabled={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
onClick={handleDiscardSaveLimit}
>
Discard
</Button>
<div className="signal-limit-save-discard-actions">
<Button
variant="solid"
size="sm"
disabled={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
loading={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
onClick={onSaveSignalLimit}
>
Save
</Button>
<Button
variant="outlined"
color="secondary"
size="sm"
disabled={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
onClick={handleDiscardSaveLimit}
>
Discard
</Button>
<span className="signal-limit-alert-helper">
You can set up an alert after saving
</span>
</div>
</div>
)}
</Form>
@@ -1425,19 +1449,18 @@ function MultiIngestionSettings(): JSX.Element {
limit?.config?.day?.size !== undefined) ||
(signalCfg.usesCount &&
limit?.config?.day?.count !== undefined)) && (
<Tooltip
title="Set alert on this limit"
placement="top"
arrow={false}
<Badge
asChild
color="cherry"
variant="outline"
testId={`set-alert-btn-${signalName}`}
className="set-alert-btn"
>
<Button
icon={<BellPlus size={14} color={Color.BG_CHERRY_400} />}
className="set-alert-btn periscope-btn ghost"
type="text"
data-testid={`set-alert-btn-${signalName}`}
onClick={onCreateSignalAlert}
/>
</Tooltip>
<Button onClick={onCreateSignalAlert} size="sm">
<BellPlus size={12} />
Set alert
</Button>
</Badge>
)}
</div>
@@ -1617,7 +1640,13 @@ function MultiIngestionSettings(): JSX.Element {
}
placement="topLeft"
>
<Button type="text" icon={<TriangleAlert size={14} />} />
<Button
variant="ghost"
size="icon"
color="secondary"
prefix={<TriangleAlert size={14} />}
aria-label="Ingestion URL error details"
/>
</Tooltip>
)}
</div>
@@ -1633,11 +1662,12 @@ function MultiIngestionSettings(): JSX.Element {
/>
<Button
variant="solid"
className="add-new-ingestion-key-btn"
type="primary"
prefix={<Plus size={14} />}
onClick={showAddModal}
>
<Plus size={14} /> New Ingestion key
New Ingestion key
</Button>
</div>
@@ -1670,15 +1700,19 @@ function MultiIngestionSettings(): JSX.Element {
footer={[
<Button
key="cancel"
variant="ghost"
color="secondary"
prefix={<X size={16} />}
onClick={hideDeleteViewModal}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
key="submit"
icon={<Trash2 size={16} />}
variant="solid"
color="destructive"
prefix={<Trash2 size={16} />}
loading={isDeleteingAPIKey}
onClick={onDeleteHandler}
className="delete-btn"
@@ -1706,15 +1740,19 @@ function MultiIngestionSettings(): JSX.Element {
footer={[
<Button
key="cancel"
variant="ghost"
color="secondary"
prefix={<X size={16} />}
onClick={hideDeleteLimitModal}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
key="submit"
icon={<Trash2 size={16} />}
variant="solid"
color="destructive"
prefix={<Trash2 size={16} />}
loading={isDeletingLimit}
onClick={onDeleteLimitHandler}
className="delete-btn"
@@ -1745,18 +1783,18 @@ function MultiIngestionSettings(): JSX.Element {
footer={[
<Button
key="cancel"
variant="ghost"
color="secondary"
prefix={<X size={16} />}
onClick={hideEditViewModal}
className="periscope-btn cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
className="periscope-btn primary"
key="submit"
type="primary"
variant="solid"
prefix={<Check size={14} />}
loading={isLoadingUpdateAPIKey}
icon={<Check size={14} />}
onClick={onUpdateApiKey}
>
Update Ingestion Key
@@ -1813,18 +1851,18 @@ function MultiIngestionSettings(): JSX.Element {
footer={[
<Button
key="cancel"
variant="ghost"
color="secondary"
prefix={<X size={16} />}
onClick={hideAddViewModal}
className="periscope-btn cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
className="periscope-btn primary"
test-id="create-new-key"
key="submit"
type="primary"
icon={<Check size={14} />}
variant="solid"
testId="create-new-key"
prefix={<Check size={14} />}
loading={isLoadingCreateAPIKey}
onClick={onCreateIngestionKey}
>
@@ -1858,7 +1896,7 @@ function MultiIngestionSettings(): JSX.Element {
]}
validateTrigger="onBlur"
>
<Input placeholder="Enter Ingestion Key name" autoFocus />
<Input placeholder="Enter Ingestion Key name" />
</Form.Item>
<Form.Item

View File

@@ -1,7 +1,13 @@
import { GatewaytypesGettableIngestionKeysDTO } from 'api/generated/services/sigNoz.schemas';
import { QueryParams } from 'constants/query';
import { rest, server } from 'mocks-server/server';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import {
fireEvent,
render,
screen,
userEvent,
waitFor,
} from 'tests/test-utils';
import { LimitProps } from 'types/api/ingestionKeys/limits/types';
import {
AllIngestionKeyProps,
@@ -40,6 +46,16 @@ const TEST_EXPIRES_AT = '2030-01-01T00:00:00Z';
const TEST_WORKSPACE_ID = 'w1';
const INGESTION_SETTINGS_ROUTE = '/ingestion-settings';
const GLOBAL_CONFIG_RESPONSE = {
status: 'success',
data: {
external_url: '',
ingestion_url: 'http://ingest.example.com',
ai_assistant_url: null,
mcp_url: null,
},
};
describe('MultiIngestionSettings Page', () => {
beforeEach(() => {
mockPush.mockClear();
@@ -71,9 +87,6 @@ describe('MultiIngestionSettings Page', () => {
});
it('navigates to create alert with metrics count threshold', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
// Arrange API response with a metrics daily count limit so the alert button is visible
const response: TestGatewayIngestionKeysResponse = {
status: 'success',
data: {
@@ -101,59 +114,48 @@ describe('MultiIngestionSettings Page', () => {
};
server.use(
rest.get('*/api/v1/global/config*', (_req, res, ctx) =>
res(ctx.status(200), ctx.json(GLOBAL_CONFIG_RESPONSE)),
),
rest.get('*/api/v2/gateway/ingestion_keys*', (_req, res, ctx) =>
res(ctx.status(200), ctx.json(response)),
),
);
// Render with initial route to test navigation
render(<MultiIngestionSettings />, undefined, {
initialRoute: INGESTION_SETTINGS_ROUTE,
});
// Wait for ingestion key to load and expand the row to show limits
await screen.findByText('Key One');
const expandButton = screen.getByRole('button', { name: /right Key One/i });
await user.click(expandButton);
fireEvent.click(screen.getByRole('button', { name: /right Key One/i }));
// Wait for limits section to render and click metrics alert button by test id
await screen.findByText('LIMITS');
const metricsAlertBtn = (await screen.findByTestId(
'set-alert-btn-metrics',
)) as HTMLButtonElement;
await user.click(metricsAlertBtn);
fireEvent.click(
(await screen.findByTestId('set-alert-btn-metrics')) as HTMLButtonElement,
);
// Wait for navigation to occur
await waitFor(() => {
expect(mockPush).toHaveBeenCalledTimes(1);
});
// Assert: navigation occurred with correct query parameters
const navigationCall = mockPush.mock.calls[0][0] as string;
// Check URL contains alerts/new route
expect(navigationCall).toContain('/alerts/new');
// Parse query parameters
const urlParams = new URLSearchParams(navigationCall.split('?')[1]);
const thresholds = JSON.parse(urlParams.get(QueryParams.thresholds) || '{}');
expect(thresholds).toBeDefined();
expect(thresholds[0].thresholdValue).toBe(1000);
// Verify compositeQuery parameter exists and contains correct data
const compositeQuery = JSON.parse(
urlParams.get(QueryParams.compositeQuery) || '{}',
);
expect(compositeQuery.builder).toBeDefined();
expect(compositeQuery.builder.queryData).toBeDefined();
// Check that the query contains the correct filter expression for the key
const firstQueryData = compositeQuery.builder.queryData[0];
expect(firstQueryData.filter.expression).toContain(
"signoz.workspace.key.id='k1'",
);
// Verify metric name for metrics signal
expect(firstQueryData.aggregations[0].metricName).toBe(
'signoz.meter.metric.datapoint.count',
);
@@ -248,6 +250,66 @@ describe('MultiIngestionSettings Page', () => {
);
});
it('shows alert CTAs in view mode and helper text in edit mode for configured limits', async () => {
const KEY_NAME = 'Key With Limits';
const response: TestGatewayIngestionKeysResponse = {
status: 'success',
data: {
keys: [
{
name: KEY_NAME,
expires_at: new Date(TEST_EXPIRES_AT),
value: 'secret',
workspace_id: TEST_WORKSPACE_ID,
id: 'k1',
created_at: new Date(TEST_CREATED_UPDATED),
updated_at: new Date(TEST_CREATED_UPDATED),
tags: [],
limits: [
{
id: 'l1',
signal: 'metrics',
config: { day: { count: 1000 } },
},
{
id: 'l2',
signal: 'logs',
config: { day: { size: 1073741824 } },
},
],
},
],
_pagination: { page: 1, per_page: 10, pages: 1, total: 1 },
},
};
server.use(
rest.get('*/api/v1/global/config*', (_req, res, ctx) =>
res(ctx.status(200), ctx.json(GLOBAL_CONFIG_RESPONSE)),
),
rest.get('*/api/v2/gateway/ingestion_keys*', (_req, res, ctx) =>
res(ctx.status(200), ctx.json(response)),
),
);
render(<MultiIngestionSettings />, undefined, {
initialRoute: INGESTION_SETTINGS_ROUTE,
});
await screen.findByText(KEY_NAME);
fireEvent.click(
screen.getByRole('button', { name: new RegExp(`right ${KEY_NAME}`, 'i') }),
);
await screen.findByText('LIMITS');
expect(screen.getAllByText('Set alert').length).toBeGreaterThan(0);
fireEvent.click(screen.getByRole('button', { name: 'Edit logs limit' }));
expect(
screen.getByText('You can set up an alert after saving'),
).toBeInTheDocument();
});
it('switches to search API when search text is entered', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
@@ -295,6 +357,9 @@ describe('MultiIngestionSettings Page', () => {
const searchHandler = jest.fn();
server.use(
rest.get('*/api/v1/global/config*', (_req, res, ctx) =>
res(ctx.status(200), ctx.json(GLOBAL_CONFIG_RESPONSE)),
),
rest.get('*/api/v2/gateway/ingestion_keys', (req, res, ctx) => {
if (req.url.pathname.endsWith('/search')) {
return undefined;