Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,21 @@ describe('updateRuleDataSchema', () => {
expect(result).toEqual({ metadata: { name: 'updated name' } });
});

it('accepts an enabled field set to true', () => {
const result = updateRuleDataSchema.parse({ enabled: true });
expect(result).toEqual({ enabled: true });
});

it('accepts an enabled field set to false', () => {
const result = updateRuleDataSchema.parse({ enabled: false });
expect(result).toEqual({ enabled: false });
});

it('omits enabled when not provided', () => {
const result = updateRuleDataSchema.parse({});
expect(result).not.toHaveProperty('enabled');
});

it('accepts a state_transition object', () => {
const result = updateRuleDataSchema.parse({
state_transition: { pending_count: 3, recovering_count: 5 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ export const updateRuleDataSchema = z
grouping: groupingSchema.optional().nullable(),
no_data: noDataSchema.optional().nullable(),
notification_policies: z.array(notificationPolicyRefSchema).optional().nullable(),
enabled: z.boolean().optional().describe('Whether the rule is enabled.'),
})
.strip();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { useMutation, useQueryClient } from '@kbn/react-query';
import { i18n } from '@kbn/i18n';
import { useService, CoreStart } from '@kbn/core-di-browser';
import { RulesApi } from '../services/rules_api';
import { ruleKeys } from './query_key_factory';

export const useToggleRuleEnabled = () => {
const rulesApi = useService(RulesApi);
const { toasts } = useService(CoreStart('notifications'));
const queryClient = useQueryClient();

return useMutation({
mutationFn: ({ id, enabled }: { id: string; enabled: boolean }) =>
rulesApi.updateRule(id, { enabled }),
onSuccess: (_data, variables) => {
toasts.addSuccess(
variables.enabled
? i18n.translate('xpack.alertingV2.hooks.useToggleRuleEnabled.enabledMessage', {
defaultMessage: 'Rule enabled',
})
: i18n.translate('xpack.alertingV2.hooks.useToggleRuleEnabled.disabledMessage', {
defaultMessage: 'Rule disabled',
})
);
queryClient.invalidateQueries(ruleKeys.lists());
queryClient.invalidateQueries(ruleKeys.detail(variables.id));
},
onError: () => {
toasts.addDanger(
i18n.translate('xpack.alertingV2.hooks.useToggleRuleEnabled.errorMessage', {
defaultMessage: 'Failed to update rule status',
})
);
},
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@ const renderEditPage = (ruleId: string = 'rule-1') => {
);
};

const renderClonePage = (sourceRuleId: string = 'rule-1') => {
return render(
<QueryClientProvider client={createQueryClient()}>
<MemoryRouter initialEntries={[`/create?cloneFrom=${sourceRuleId}`]}>
<I18nProvider>
<Route path="/create">
<RuleFormPage />
</Route>
</I18nProvider>
</MemoryRouter>
</QueryClientProvider>
);
};

describe('RuleFormPage', () => {
beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -272,4 +286,186 @@ describe('RuleFormPage', () => {
expect(capturedStandaloneProps.query).toBe('FROM metrics-* | LIMIT 10');
});
});

describe('clone mode', () => {
it('shows loading spinner while fetching source rule', () => {
mockUseFetchRule.mockReturnValue({
data: undefined,
isLoading: true,
isError: false,
error: null,
});

renderClonePage();

expect(screen.getByRole('progressbar')).toBeInTheDocument();
});

it('shows error state when source rule fetch fails', () => {
mockUseFetchRule.mockReturnValue({
data: undefined,
isLoading: false,
isError: true,
error: new Error('Rule not found'),
});

renderClonePage();

expect(screen.getByText('Failed to load source rule for cloning')).toBeInTheDocument();
expect(screen.getByText('Rule not found')).toBeInTheDocument();
});

it('renders the "Clone rule" page title', () => {
mockUseFetchRule.mockReturnValue({
data: {
id: 'rule-1',
kind: 'alert',
enabled: true,
metadata: { name: 'Source Rule' },
time_field: '@timestamp',
schedule: { every: '1m', lookback: '5m' },
evaluation: { query: { base: 'FROM logs-* | LIMIT 1' } },
},
isLoading: false,
isError: false,
error: null,
});

renderClonePage();

expect(screen.getByRole('heading', { name: 'Clone rule' })).toBeInTheDocument();
});

it('does not pass ruleId to StandaloneRuleForm (creates a new rule)', () => {
mockUseFetchRule.mockReturnValue({
data: {
id: 'rule-1',
kind: 'alert',
enabled: true,
metadata: { name: 'Source Rule' },
time_field: '@timestamp',
schedule: { every: '1m', lookback: '5m' },
evaluation: { query: { base: 'FROM logs-* | LIMIT 1' } },
},
isLoading: false,
isError: false,
error: null,
});

renderClonePage();

expect(capturedStandaloneProps.ruleId).toBeUndefined();
});

it('appends " (clone)" to the rule name in initialValues', () => {
mockUseFetchRule.mockReturnValue({
data: {
id: 'rule-1',
kind: 'alert',
enabled: true,
metadata: { name: 'My Alert Rule', labels: ['prod'], owner: 'team-a' },
time_field: '@timestamp',
schedule: { every: '10m', lookback: '2m' },
evaluation: {
query: {
base: 'FROM logs-* | STATS count() BY host.name',
condition: 'WHERE count > 5',
},
},
grouping: { fields: ['host.name'] },
recovery_policy: { type: 'no_breach' },
state_transition: { pending_count: 3, pending_timeframe: '5m' },
},
isLoading: false,
isError: false,
error: null,
});

renderClonePage();

const initialValues = capturedStandaloneProps.initialValues as Record<string, unknown>;
expect(initialValues).toBeDefined();

const metadata = initialValues.metadata as Record<string, unknown>;
expect(metadata.name).toBe('My Alert Rule (clone)');
});

it('preserves all source rule configurations in initialValues', () => {
mockUseFetchRule.mockReturnValue({
data: {
id: 'rule-1',
kind: 'alert',
enabled: true,
metadata: { name: 'My Alert Rule', labels: ['prod'], owner: 'team-a' },
time_field: '@timestamp',
schedule: { every: '10m', lookback: '2m' },
evaluation: {
query: {
base: 'FROM logs-* | STATS count() BY host.name',
condition: 'WHERE count > 5',
},
},
grouping: { fields: ['host.name'] },
recovery_policy: { type: 'no_breach' },
state_transition: { pending_count: 3, pending_timeframe: '5m' },
},
isLoading: false,
isError: false,
error: null,
});

renderClonePage();

const initialValues = capturedStandaloneProps.initialValues as Record<string, unknown>;

// Metadata (labels, owner preserved)
const metadata = initialValues.metadata as Record<string, unknown>;
expect(metadata.labels).toEqual(['prod']);
expect(metadata.owner).toBe('team-a');

// Schedule
const schedule = initialValues.schedule as Record<string, unknown>;
expect(schedule.every).toBe('10m');
expect(schedule.lookback).toBe('2m');

// Evaluation
const evaluation = initialValues.evaluation as { query: Record<string, unknown> };
expect(evaluation.query.base).toBe('FROM logs-* | STATS count() BY host.name');
expect(evaluation.query.condition).toBe('WHERE count > 5');

// Grouping
const grouping = initialValues.grouping as { fields: string[] };
expect(grouping.fields).toEqual(['host.name']);

// Recovery policy
const recoveryPolicy = initialValues.recoveryPolicy as Record<string, unknown>;
expect(recoveryPolicy.type).toBe('no_breach');

// State transition
const stateTransition = initialValues.stateTransition as Record<string, unknown>;
expect(stateTransition.pendingCount).toBe(3);
expect(stateTransition.pendingTimeframe).toBe('5m');
});

it('passes the base query from the source rule as the query prop', () => {
mockUseFetchRule.mockReturnValue({
data: {
id: 'rule-1',
kind: 'alert',
enabled: true,
metadata: { name: 'Test' },
time_field: '@timestamp',
schedule: { every: '5m' },
evaluation: { query: { base: 'FROM metrics-* | LIMIT 10' } },
},
isLoading: false,
isError: false,
error: null,
});

renderClonePage();

expect(capturedStandaloneProps.query).toBe('FROM metrics-* | LIMIT 10');
});
});
});
Loading
Loading