mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-19 16:40:42 +01:00
Compare commits
2 Commits
fix/dynami
...
frontend/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0f6e844fe | ||
|
|
e4e58e2349 |
@@ -25,6 +25,7 @@ export const REACT_QUERY_KEY = {
|
||||
ALERT_RULES_CHART_PREVIEW: 'ALERT_RULES_CHART_PREVIEW',
|
||||
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
|
||||
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
|
||||
CLONE_ALERT_RULE: 'CLONE_ALERT_RULE',
|
||||
GET_HOST_LIST: 'GET_HOST_LIST',
|
||||
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
|
||||
GET_TRACE_V2_WATERFALL: 'GET_TRACE_V2_WATERFALL',
|
||||
|
||||
@@ -2,6 +2,7 @@ import userEvent from '@testing-library/user-event';
|
||||
import { logEventMock } from '__tests__/logEventMock';
|
||||
import { safeNavigateMock } from '__tests__/safeNavigateMock';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { alertRulesFixture } from 'mocks-server/__mockdata__/alert_rules';
|
||||
import { rest } from 'msw';
|
||||
import { screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
@@ -121,22 +122,26 @@ describe('ListAlertRules — actions menu', () => {
|
||||
expect(options).toStrictEqual(expect.objectContaining({ newTab: true }));
|
||||
});
|
||||
|
||||
it('clone action: sends POST with " - Copy" suffix and opens the cloned rule returned by the API', async () => {
|
||||
let capturedPostBody: unknown = null;
|
||||
it('clone action: sends POST to clone endpoint and opens the cloned rule returned by the API', async () => {
|
||||
let capturedRuleId: string | null = null;
|
||||
server.use(
|
||||
rest.post('http://localhost/api/v2/rules', async (req, res, ctx) => {
|
||||
capturedPostBody = await req.json();
|
||||
return res(
|
||||
ctx.status(201),
|
||||
ctx.json({
|
||||
data: {
|
||||
...(capturedPostBody as Record<string, unknown>),
|
||||
id: 'cloned-from-server',
|
||||
},
|
||||
status: 'success',
|
||||
}),
|
||||
);
|
||||
}),
|
||||
rest.post(
|
||||
'http://localhost/api/v2/rules/:id/clone',
|
||||
async (req, res, ctx) => {
|
||||
capturedRuleId = req.params.id as string;
|
||||
return res(
|
||||
ctx.status(201),
|
||||
ctx.json({
|
||||
data: {
|
||||
...alertRulesFixture[0],
|
||||
id: 'cloned-from-server',
|
||||
alert: 'High CPU Alert - Copy',
|
||||
},
|
||||
status: 'success',
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
renderListAlertRules();
|
||||
@@ -145,9 +150,7 @@ describe('ListAlertRules — actions menu', () => {
|
||||
await clickMenuItem('Clone');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPostBody).toStrictEqual(
|
||||
expect.objectContaining({ alert: 'High CPU Alert - Copy' }),
|
||||
);
|
||||
expect(capturedRuleId).toBe('rule-1');
|
||||
});
|
||||
|
||||
// The id from the server response round-trips into the navigate URL — this
|
||||
|
||||
@@ -6,10 +6,10 @@ import { Dropdown, type DropdownItemType } from '@signozhq/ui/dropdown';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
createRule,
|
||||
deleteRuleByID,
|
||||
invalidateListRules,
|
||||
patchRuleByID,
|
||||
cloneRuleByID,
|
||||
} from 'api/generated/services/rules';
|
||||
import type {
|
||||
RenderErrorResponseDTO,
|
||||
@@ -62,10 +62,9 @@ function ActionsMenu({ rule, onEdit }: ActionsMenuProps): JSX.Element {
|
||||
const handleClone = useCallback((): void => {
|
||||
alertActionLogEvent(ALERT_ACTIONS.CLONE, rule);
|
||||
toast.promise(
|
||||
createRule({
|
||||
...rule,
|
||||
alert: `${rule.alert} - Copy`,
|
||||
} as RuletypesPostableRuleDTO).then(async (response) => {
|
||||
cloneRuleByID({
|
||||
id: rule.id,
|
||||
}).then(async (response) => {
|
||||
await invalidateListRules(queryClient);
|
||||
const newRule = response.data;
|
||||
if (newRule) {
|
||||
|
||||
@@ -7,8 +7,8 @@ import { Switch } from '@signozhq/ui/switch';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { Copy, Ellipsis, PenLine, Trash2 } from '@signozhq/icons';
|
||||
import {
|
||||
useAlertRuleClone,
|
||||
useAlertRuleDelete,
|
||||
useAlertRuleDuplicate,
|
||||
useAlertRuleStatusToggle,
|
||||
useAlertRuleUpdate,
|
||||
} from 'pages/AlertDetails/hooks';
|
||||
@@ -38,9 +38,7 @@ function AlertActionButtons({
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const { handleAlertStateToggle } = useAlertRuleStatusToggle({ ruleId });
|
||||
const { handleAlertDuplicate } = useAlertRuleDuplicate({
|
||||
alertDetails: alertDetails as unknown as AlertDef,
|
||||
});
|
||||
const { handleAlertClone } = useAlertRuleClone({ ruleId });
|
||||
const { handleAlertDelete } = useAlertRuleDelete({ ruleId });
|
||||
const { handleAlertUpdate, isLoading } = useAlertRuleUpdate({
|
||||
alertDetails: alertDetails as unknown as AlertDef,
|
||||
@@ -72,10 +70,10 @@ function AlertActionButtons({
|
||||
menuItems.push(
|
||||
{
|
||||
type: 'item',
|
||||
value: 'duplicate-rule',
|
||||
label: 'Duplicate',
|
||||
value: 'clone-rule',
|
||||
label: 'Clone',
|
||||
prefix: <Copy size={16} color={Color.BG_VANILLA_400} />,
|
||||
onClick: handleAlertDuplicate,
|
||||
onClick: handleAlertClone,
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { FilterValue, SorterResult } from 'antd/es/table/interface';
|
||||
import { patchRulePartial } from 'api/alerts/patchRulePartial';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
createRule,
|
||||
cloneRuleByID,
|
||||
deleteRuleByID,
|
||||
getGetRuleByIDQueryKey,
|
||||
getGetRuleHistoryTimelineQueryOptions,
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
useGetRuleHistoryOverallStatus,
|
||||
useGetRuleHistoryStats,
|
||||
useGetRuleHistoryTopContributors,
|
||||
useListRules,
|
||||
} from 'api/generated/services/rules';
|
||||
import {
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
@@ -478,37 +477,33 @@ export const useAlertRuleStatusToggle = ({
|
||||
return { handleAlertStateToggle };
|
||||
};
|
||||
|
||||
export const useAlertRuleDuplicate = ({
|
||||
alertDetails,
|
||||
export const useAlertRuleClone = ({
|
||||
ruleId,
|
||||
}: {
|
||||
alertDetails: AlertDef;
|
||||
ruleId: string;
|
||||
}): {
|
||||
handleAlertDuplicate: () => void;
|
||||
handleAlertClone: () => void;
|
||||
} => {
|
||||
const { notifications } = useNotifications();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const params = useUrlQuery();
|
||||
|
||||
const { refetch } = useListRules({
|
||||
query: { cacheTime: 0 },
|
||||
});
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const { mutate: duplicateAlert } = useMutation(
|
||||
[REACT_QUERY_KEY.DUPLICATE_ALERT_RULE],
|
||||
(args: { data: AlertDef }) =>
|
||||
createRule(toPostableRuleDTOFromAlertDef(args.data)),
|
||||
const { mutate: cloneAlert } = useMutation(
|
||||
[REACT_QUERY_KEY.CLONE_ALERT_RULE],
|
||||
() => cloneRuleByID({ id: ruleId }),
|
||||
{
|
||||
onSuccess: async () => {
|
||||
onSuccess: async (response) => {
|
||||
notifications.success({
|
||||
message: `Success`,
|
||||
});
|
||||
|
||||
const { data: allAlertsData } = await refetch();
|
||||
await invalidateListRules(queryClient);
|
||||
|
||||
const rules = allAlertsData?.data;
|
||||
if (rules && rules.length > 0) {
|
||||
const clonedAlert = rules[rules.length - 1];
|
||||
params.set(QueryParams.ruleId, String(clonedAlert.id));
|
||||
const clonedRule = response?.data;
|
||||
if (clonedRule?.id) {
|
||||
params.set(QueryParams.ruleId, String(clonedRule.id));
|
||||
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||
}
|
||||
},
|
||||
@@ -519,15 +514,15 @@ export const useAlertRuleDuplicate = ({
|
||||
},
|
||||
);
|
||||
|
||||
const handleAlertDuplicate = (): void => {
|
||||
const args = {
|
||||
data: { ...alertDetails, alert: alertDetails.alert?.concat(' - Copy') },
|
||||
};
|
||||
duplicateAlert(args);
|
||||
const handleAlertClone = (): void => {
|
||||
cloneAlert();
|
||||
};
|
||||
|
||||
return { handleAlertDuplicate };
|
||||
return { handleAlertClone };
|
||||
};
|
||||
|
||||
// Kept for backwards compatibility
|
||||
export const useAlertRuleDuplicate = useAlertRuleClone;
|
||||
export const useAlertRuleUpdate = ({
|
||||
alertDetails,
|
||||
setAlertRuleName,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
@@ -7,6 +8,10 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { getParsedAggregationOptionsForOrderBy } from 'utils/aggregationConverter';
|
||||
import { sanitizeOrderByForExplorer } from 'utils/sanitizeOrderBy';
|
||||
|
||||
jest.mock('@sentry/react', () => ({
|
||||
captureEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('utils/aggregationConverter', () => ({
|
||||
getParsedAggregationOptionsForOrderBy: jest.fn(),
|
||||
}));
|
||||
@@ -117,6 +122,7 @@ describe('sanitizeOrderByForExplorer', () => {
|
||||
|
||||
const result = sanitizeOrderByForExplorer(query);
|
||||
expect(result).toStrictEqual([]);
|
||||
expect(Sentry.captureEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles missing orderBy by returning an empty array', () => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
OrderByPayload,
|
||||
@@ -20,15 +19,5 @@ export function sanitizeOrderByForExplorer(
|
||||
|
||||
const current = query.orderBy || [];
|
||||
|
||||
const hasInvalidOrderBy = current.some((o) => !allowed.has(o.columnName));
|
||||
|
||||
if (hasInvalidOrderBy) {
|
||||
Sentry.captureEvent({
|
||||
message: `Invalid orderBy: current: ${JSON.stringify(
|
||||
current,
|
||||
)} - allowed: ${JSON.stringify(Array.from(allowed))}`,
|
||||
level: 'warning',
|
||||
});
|
||||
}
|
||||
return current.filter((o) => allowed.has(o.columnName));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user