mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Response Ops][Rules] Add New Rule Form to Stack Management (#194655)](https://github.com/elastic/kibana/pull/194655) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Jiawei Wu","email":"74562234+JiaweiWu@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-10-15T10:17:59Z","message":"[Response Ops][Rules] Add New Rule Form to Stack Management (#194655)\n\n## Summary\r\n\r\nEnables and adds the new rule form to stack management. We are only\r\ngoing to turn this on for stack management for now until we are\r\nconfident that this is fairly bug free.\r\n\r\n### To test:\r\n\r\n1. Switch `USE_NEW_RULE_FORM_FEATURE_FLAG` to true\r\n2. Navigate to stack management -> rules list\r\n3. Click \"Create rule\" \r\n4. Assert the user is navigated to the new form\r\n5. Create rule\r\n6. Assert the user is navigated to the rule details page\r\n7. Click \"Edit\"\r\n8. Edit rule\r\n9. Assert the user is navigated to the rule details page\r\n10. Try editing a rule in the rules list and assert everything works as\r\nexpected\r\n\r\nWe should also make sure this rule form is not enabled in other\r\nsolutions.\r\n\r\n### Checklist\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>\r\nCo-authored-by: Christos Nasikas <christos.nasikas@elastic.co>","sha":"5c2df6347d779f577946634e972d30224299079a","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:ResponseOps","v9.0.0","backport:prev-minor","v8.16.0"],"number":194655,"url":"https://github.com/elastic/kibana/pull/194655","mergeCommit":{"message":"[Response Ops][Rules] Add New Rule Form to Stack Management (#194655)\n\n## Summary\r\n\r\nEnables and adds the new rule form to stack management. We are only\r\ngoing to turn this on for stack management for now until we are\r\nconfident that this is fairly bug free.\r\n\r\n### To test:\r\n\r\n1. Switch `USE_NEW_RULE_FORM_FEATURE_FLAG` to true\r\n2. Navigate to stack management -> rules list\r\n3. Click \"Create rule\" \r\n4. Assert the user is navigated to the new form\r\n5. Create rule\r\n6. Assert the user is navigated to the rule details page\r\n7. Click \"Edit\"\r\n8. Edit rule\r\n9. Assert the user is navigated to the rule details page\r\n10. Try editing a rule in the rules list and assert everything works as\r\nexpected\r\n\r\nWe should also make sure this rule form is not enabled in other\r\nsolutions.\r\n\r\n### Checklist\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>\r\nCo-authored-by: Christos Nasikas <christos.nasikas@elastic.co>","sha":"5c2df6347d779f577946634e972d30224299079a"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/194655","number":194655,"mergeCommit":{"message":"[Response Ops][Rules] Add New Rule Form to Stack Management (#194655)\n\n## Summary\r\n\r\nEnables and adds the new rule form to stack management. We are only\r\ngoing to turn this on for stack management for now until we are\r\nconfident that this is fairly bug free.\r\n\r\n### To test:\r\n\r\n1. Switch `USE_NEW_RULE_FORM_FEATURE_FLAG` to true\r\n2. Navigate to stack management -> rules list\r\n3. Click \"Create rule\" \r\n4. Assert the user is navigated to the new form\r\n5. Create rule\r\n6. Assert the user is navigated to the rule details page\r\n7. Click \"Edit\"\r\n8. Edit rule\r\n9. Assert the user is navigated to the rule details page\r\n10. Try editing a rule in the rules list and assert everything works as\r\nexpected\r\n\r\nWe should also make sure this rule form is not enabled in other\r\nsolutions.\r\n\r\n### Checklist\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>\r\nCo-authored-by: Christos Nasikas <christos.nasikas@elastic.co>","sha":"5c2df6347d779f577946634e972d30224299079a"}},{"branch":"8.x","label":"v8.16.0","labelRegex":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com>
This commit is contained in:
parent
bf0432de4e
commit
99bddf8fa6
60 changed files with 856 additions and 296 deletions
|
@ -54,6 +54,6 @@ export const transformUpdateRuleBody: RewriteResponseCase<UpdateRuleBody> = ({
|
|||
...(uuid && { uuid }),
|
||||
};
|
||||
}),
|
||||
...(alertDelay ? { alert_delay: alertDelay } : {}),
|
||||
...(alertDelay !== undefined ? { alert_delay: alertDelay } : {}),
|
||||
...(flapping !== undefined ? { flapping: transformUpdateRuleFlapping(flapping) } : {}),
|
||||
});
|
||||
|
|
|
@ -8,4 +8,4 @@
|
|||
*/
|
||||
|
||||
// Feature flag for frontend rule specific flapping in rule flyout
|
||||
export const IS_RULE_SPECIFIC_FLAPPING_ENABLED = false;
|
||||
export const IS_RULE_SPECIFIC_FLAPPING_ENABLED = true;
|
||||
|
|
|
@ -10,10 +10,11 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { HttpStart, IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { createRule, CreateRuleBody } from '../apis/create_rule';
|
||||
import { Rule } from '../types';
|
||||
|
||||
export interface UseCreateRuleProps {
|
||||
http: HttpStart;
|
||||
onSuccess?: (formData: CreateRuleBody) => void;
|
||||
onSuccess?: (rule: Rule) => void;
|
||||
onError?: (error: IHttpFetchError<{ message: string }>) => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,10 +15,11 @@ export interface UseLoadConnectorsProps {
|
|||
http: HttpStart;
|
||||
includeSystemActions?: boolean;
|
||||
enabled?: boolean;
|
||||
cacheTime?: number;
|
||||
}
|
||||
|
||||
export const useLoadConnectors = (props: UseLoadConnectorsProps) => {
|
||||
const { http, includeSystemActions = false, enabled = true } = props;
|
||||
const { http, includeSystemActions = false, enabled = true, cacheTime } = props;
|
||||
|
||||
const queryFn = () => {
|
||||
return fetchConnectors({ http, includeSystemActions });
|
||||
|
@ -27,6 +28,7 @@ export const useLoadConnectors = (props: UseLoadConnectorsProps) => {
|
|||
const { data, isLoading, isFetching, isInitialLoading } = useQuery({
|
||||
queryKey: ['useLoadConnectors', includeSystemActions],
|
||||
queryFn,
|
||||
cacheTime,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled,
|
||||
});
|
||||
|
|
|
@ -17,10 +17,11 @@ export interface UseLoadRuleTypeAadTemplateFieldProps {
|
|||
http: HttpStart;
|
||||
ruleTypeId?: string;
|
||||
enabled: boolean;
|
||||
cacheTime?: number;
|
||||
}
|
||||
|
||||
export const useLoadRuleTypeAadTemplateField = (props: UseLoadRuleTypeAadTemplateFieldProps) => {
|
||||
const { http, ruleTypeId, enabled } = props;
|
||||
const { http, ruleTypeId, enabled, cacheTime } = props;
|
||||
|
||||
const queryFn = () => {
|
||||
if (!ruleTypeId) {
|
||||
|
@ -43,6 +44,7 @@ export const useLoadRuleTypeAadTemplateField = (props: UseLoadRuleTypeAadTemplat
|
|||
description: getDescription(d.name, EcsFlat),
|
||||
}));
|
||||
},
|
||||
cacheTime,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled,
|
||||
});
|
||||
|
|
|
@ -15,10 +15,11 @@ import { RuleFormData } from '../../rule_form';
|
|||
export interface UseResolveProps {
|
||||
http: HttpStart;
|
||||
id?: string;
|
||||
cacheTime?: number;
|
||||
}
|
||||
|
||||
export const useResolveRule = (props: UseResolveProps) => {
|
||||
const { id, http } = props;
|
||||
const { id, http, cacheTime } = props;
|
||||
|
||||
const queryFn = () => {
|
||||
if (id) {
|
||||
|
@ -30,6 +31,7 @@ export const useResolveRule = (props: UseResolveProps) => {
|
|||
queryKey: ['useResolveRule', id],
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
cacheTime,
|
||||
select: (rule): RuleFormData | null => {
|
||||
if (!rule) {
|
||||
return null;
|
||||
|
|
|
@ -10,10 +10,11 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { HttpStart, IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { updateRule, UpdateRuleBody } from '../apis/update_rule';
|
||||
import { Rule } from '../types';
|
||||
|
||||
export interface UseUpdateRuleProps {
|
||||
http: HttpStart;
|
||||
onSuccess?: (formData: UpdateRuleBody) => void;
|
||||
onSuccess?: (rule: Rule) => void;
|
||||
onError?: (error: IHttpFetchError<{ message: string }>) => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,8 +27,6 @@ import { TypeRegistry } from '../type_registry';
|
|||
|
||||
export type { SanitizedRuleAction as RuleAction } from '@kbn/alerting-types';
|
||||
|
||||
export type { Flapping } from '@kbn/alerting-types';
|
||||
|
||||
export type RuleTypeWithDescription = RuleType<string, string> & { description?: string };
|
||||
|
||||
export type RuleTypeIndexWithDescriptions = Map<string, RuleTypeWithDescription>;
|
||||
|
|
|
@ -27,7 +27,7 @@ export const DEFAULT_FREQUENCY = {
|
|||
summary: false,
|
||||
};
|
||||
|
||||
export const GET_DEFAULT_FORM_DATA = ({
|
||||
export const getDefaultFormData = ({
|
||||
ruleTypeId,
|
||||
name,
|
||||
consumer,
|
||||
|
@ -50,6 +50,7 @@ export const GET_DEFAULT_FORM_DATA = ({
|
|||
ruleTypeId,
|
||||
name,
|
||||
actions,
|
||||
alertDelay: { active: 1 },
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { EuiLoadingElastic } from '@elastic/eui';
|
|||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { type RuleCreationValidConsumer } from '@kbn/rule-data-utils';
|
||||
import type { RuleFormData, RuleFormPlugins } from './types';
|
||||
import { DEFAULT_VALID_CONSUMERS, GET_DEFAULT_FORM_DATA } from './constants';
|
||||
import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants';
|
||||
import { RuleFormStateProvider } from './rule_form_state';
|
||||
import { useCreateRule } from '../common/hooks';
|
||||
import { RulePage } from './rule_page';
|
||||
|
@ -24,6 +24,7 @@ import {
|
|||
} from './rule_form_errors';
|
||||
import { useLoadDependencies } from './hooks/use_load_dependencies';
|
||||
import {
|
||||
getAvailableRuleTypes,
|
||||
getInitialConsumer,
|
||||
getInitialMultiConsumer,
|
||||
getInitialSchedule,
|
||||
|
@ -42,7 +43,8 @@ export interface CreateRuleFormProps {
|
|||
shouldUseRuleProducer?: boolean;
|
||||
canShowConsumerSelection?: boolean;
|
||||
showMustacheAutocompleteSwitch?: boolean;
|
||||
returnUrl: string;
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (ruleId: string) => void;
|
||||
}
|
||||
|
||||
export const CreateRuleForm = (props: CreateRuleFormProps) => {
|
||||
|
@ -56,7 +58,8 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
|
|||
shouldUseRuleProducer = false,
|
||||
canShowConsumerSelection = true,
|
||||
showMustacheAutocompleteSwitch = false,
|
||||
returnUrl,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
} = props;
|
||||
|
||||
const { http, docLinks, notifications, ruleTypeRegistry, i18n, theme } = plugins;
|
||||
|
@ -64,8 +67,9 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
|
|||
|
||||
const { mutate, isLoading: isSaving } = useCreateRule({
|
||||
http,
|
||||
onSuccess: ({ name }) => {
|
||||
onSuccess: ({ name, id }) => {
|
||||
toasts.addSuccess(RULE_CREATE_SUCCESS_TEXT(name));
|
||||
onSubmit?.(id);
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = parseRuleCircuitBreakerErrorMessage(
|
||||
|
@ -86,6 +90,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
|
|||
const {
|
||||
isInitialLoading,
|
||||
ruleType,
|
||||
ruleTypes,
|
||||
ruleTypeModel,
|
||||
uiConfig,
|
||||
healthCheckError,
|
||||
|
@ -153,7 +158,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
|
|||
<div data-test-subj="createRuleForm">
|
||||
<RuleFormStateProvider
|
||||
initialRuleFormState={{
|
||||
formData: GET_DEFAULT_FORM_DATA({
|
||||
formData: getDefaultFormData({
|
||||
ruleTypeId,
|
||||
name: `${ruleType.name} rule`,
|
||||
consumer: getInitialConsumer({
|
||||
|
@ -174,6 +179,11 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
|
|||
minimumScheduleInterval: uiConfig?.minimumScheduleInterval,
|
||||
selectedRuleTypeModel: ruleTypeModel,
|
||||
selectedRuleType: ruleType,
|
||||
availableRuleTypes: getAvailableRuleTypes({
|
||||
consumer,
|
||||
ruleTypes,
|
||||
ruleTypeRegistry,
|
||||
}).map(({ ruleType: rt }) => rt),
|
||||
validConsumers,
|
||||
flappingSettings,
|
||||
canShowConsumerSelection,
|
||||
|
@ -185,7 +195,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
|
|||
}),
|
||||
}}
|
||||
>
|
||||
<RulePage isEdit={false} isSaving={isSaving} returnUrl={returnUrl} onSave={onSave} />
|
||||
<RulePage isEdit={false} isSaving={isSaving} onCancel={onCancel} onSave={onSave} />
|
||||
</RuleFormStateProvider>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -24,17 +24,19 @@ import {
|
|||
RuleFormRuleTypeError,
|
||||
} from './rule_form_errors';
|
||||
import { RULE_EDIT_ERROR_TEXT, RULE_EDIT_SUCCESS_TEXT } from './translations';
|
||||
import { parseRuleCircuitBreakerErrorMessage } from './utils';
|
||||
import { getAvailableRuleTypes, parseRuleCircuitBreakerErrorMessage } from './utils';
|
||||
import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants';
|
||||
|
||||
export interface EditRuleFormProps {
|
||||
id: string;
|
||||
plugins: RuleFormPlugins;
|
||||
showMustacheAutocompleteSwitch?: boolean;
|
||||
returnUrl: string;
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (ruleId: string) => void;
|
||||
}
|
||||
|
||||
export const EditRuleForm = (props: EditRuleFormProps) => {
|
||||
const { id, plugins, returnUrl, showMustacheAutocompleteSwitch = false } = props;
|
||||
const { id, plugins, showMustacheAutocompleteSwitch = false, onCancel, onSubmit } = props;
|
||||
const { http, notifications, docLinks, ruleTypeRegistry, i18n, theme, application } = plugins;
|
||||
const { toasts } = notifications;
|
||||
|
||||
|
@ -42,6 +44,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
|
|||
http,
|
||||
onSuccess: ({ name }) => {
|
||||
toasts.addSuccess(RULE_EDIT_SUCCESS_TEXT(name));
|
||||
onSubmit?.(id);
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = parseRuleCircuitBreakerErrorMessage(
|
||||
|
@ -62,6 +65,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
|
|||
const {
|
||||
isInitialLoading,
|
||||
ruleType,
|
||||
ruleTypes,
|
||||
ruleTypeModel,
|
||||
uiConfig,
|
||||
healthCheckError,
|
||||
|
@ -156,17 +160,31 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
|
|||
connectors,
|
||||
connectorTypes,
|
||||
aadTemplateFields,
|
||||
formData: fetchedFormData,
|
||||
formData: {
|
||||
...getDefaultFormData({
|
||||
ruleTypeId: fetchedFormData.ruleTypeId,
|
||||
name: fetchedFormData.name,
|
||||
consumer: fetchedFormData.consumer,
|
||||
actions: fetchedFormData.actions,
|
||||
}),
|
||||
...fetchedFormData,
|
||||
},
|
||||
id,
|
||||
plugins,
|
||||
minimumScheduleInterval: uiConfig?.minimumScheduleInterval,
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleTypeModel,
|
||||
availableRuleTypes: getAvailableRuleTypes({
|
||||
consumer: fetchedFormData.consumer,
|
||||
ruleTypes,
|
||||
ruleTypeRegistry,
|
||||
}).map(({ ruleType: rt }) => rt),
|
||||
flappingSettings,
|
||||
validConsumers: DEFAULT_VALID_CONSUMERS,
|
||||
showMustacheAutocompleteSwitch,
|
||||
}}
|
||||
>
|
||||
<RulePage isEdit={true} isSaving={isSaving} returnUrl={returnUrl} onSave={onSave} />
|
||||
<RulePage isEdit={true} isSaving={isSaving} onSave={onSave} onCancel={onCancel} />
|
||||
</RuleFormStateProvider>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -46,10 +46,6 @@ jest.mock('../../common/hooks/use_load_rule_type_aad_template_fields', () => ({
|
|||
useLoadRuleTypeAadTemplateField: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../utils/get_authorized_rule_types', () => ({
|
||||
getAvailableRuleTypes: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../common/hooks/use_fetch_flapping_settings', () => ({
|
||||
useFetchFlappingSettings: jest.fn(),
|
||||
}));
|
||||
|
@ -63,7 +59,6 @@ const { useLoadRuleTypeAadTemplateField } = jest.requireMock(
|
|||
'../../common/hooks/use_load_rule_type_aad_template_fields'
|
||||
);
|
||||
const { useLoadRuleTypesQuery } = jest.requireMock('../../common/hooks/use_load_rule_types_query');
|
||||
const { getAvailableRuleTypes } = jest.requireMock('../utils/get_authorized_rule_types');
|
||||
const { useFetchFlappingSettings } = jest.requireMock(
|
||||
'../../common/hooks/use_fetch_flapping_settings'
|
||||
);
|
||||
|
@ -168,13 +163,6 @@ useLoadRuleTypesQuery.mockReturnValue({
|
|||
},
|
||||
});
|
||||
|
||||
getAvailableRuleTypes.mockReturnValue([
|
||||
{
|
||||
ruleType: indexThresholdRuleType,
|
||||
ruleTypeModel: indexThresholdRuleTypeModel,
|
||||
},
|
||||
]);
|
||||
|
||||
const mockConnector = {
|
||||
id: 'test-connector',
|
||||
name: 'Test',
|
||||
|
@ -236,7 +224,7 @@ const toastsMock = jest.fn();
|
|||
const ruleTypeRegistryMock: RuleTypeRegistryContract = {
|
||||
has: jest.fn(),
|
||||
register: jest.fn(),
|
||||
get: jest.fn(),
|
||||
get: jest.fn().mockReturnValue(indexThresholdRuleTypeModel),
|
||||
list: jest.fn(),
|
||||
};
|
||||
|
||||
|
@ -272,6 +260,7 @@ describe('useLoadDependencies', () => {
|
|||
isLoading: false,
|
||||
isInitialLoading: false,
|
||||
ruleType: indexThresholdRuleType,
|
||||
ruleTypes: [...ruleTypeIndex.values()],
|
||||
ruleTypeModel: indexThresholdRuleTypeModel,
|
||||
uiConfig: uiConfigMock,
|
||||
healthCheckError: null,
|
||||
|
@ -317,39 +306,6 @@ describe('useLoadDependencies', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('should call getAvailableRuleTypes with the correct params', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
return useLoadDependencies({
|
||||
http: httpMock as unknown as HttpStart,
|
||||
toasts: toastsMock as unknown as ToastsStart,
|
||||
ruleTypeRegistry: ruleTypeRegistryMock,
|
||||
validConsumers: ['stackAlerts', 'logs'],
|
||||
consumer: 'logs',
|
||||
capabilities: {
|
||||
actions: {
|
||||
show: true,
|
||||
save: true,
|
||||
execute: true,
|
||||
},
|
||||
} as unknown as ApplicationStart['capabilities'],
|
||||
});
|
||||
},
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
return expect(result.current.isInitialLoading).toEqual(false);
|
||||
});
|
||||
|
||||
expect(getAvailableRuleTypes).toBeCalledWith({
|
||||
consumer: 'logs',
|
||||
ruleTypeRegistry: ruleTypeRegistryMock,
|
||||
ruleTypes: [indexThresholdRuleType],
|
||||
validConsumers: ['stackAlerts', 'logs'],
|
||||
});
|
||||
});
|
||||
|
||||
test('should call resolve rule with the correct params', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
|
@ -377,6 +333,7 @@ describe('useLoadDependencies', () => {
|
|||
expect(useResolveRule).toBeCalledWith({
|
||||
http: httpMock,
|
||||
id: 'test-rule-id',
|
||||
cacheTime: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ import {
|
|||
useLoadUiConfig,
|
||||
useResolveRule,
|
||||
} from '../../common/hooks';
|
||||
import { getAvailableRuleTypes } from '../utils';
|
||||
import { RuleTypeRegistryContract } from '../../common';
|
||||
import { useFetchFlappingSettings } from '../../common/hooks/use_fetch_flapping_settings';
|
||||
import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../common/constants/rule_flapping';
|
||||
|
@ -43,8 +42,6 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
|
|||
http,
|
||||
toasts,
|
||||
ruleTypeRegistry,
|
||||
consumer,
|
||||
validConsumers,
|
||||
id,
|
||||
ruleTypeId,
|
||||
capabilities,
|
||||
|
@ -69,7 +66,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
|
|||
data: fetchedFormData,
|
||||
isLoading: isLoadingRule,
|
||||
isInitialLoading: isInitialLoadingRule,
|
||||
} = useResolveRule({ http, id });
|
||||
} = useResolveRule({ http, id, cacheTime: 0 });
|
||||
|
||||
const {
|
||||
ruleTypesState: {
|
||||
|
@ -100,6 +97,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
|
|||
http,
|
||||
includeSystemActions: true,
|
||||
enabled: canReadConnectors,
|
||||
cacheTime: 0,
|
||||
});
|
||||
|
||||
const computedRuleTypeId = useMemo(() => {
|
||||
|
@ -125,28 +123,22 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
|
|||
http,
|
||||
ruleTypeId: computedRuleTypeId,
|
||||
enabled: !!computedRuleTypeId && canReadConnectors,
|
||||
cacheTime: 0,
|
||||
});
|
||||
|
||||
const authorizedRuleTypeItems = useMemo(() => {
|
||||
const computedConsumer = consumer || fetchedFormData?.consumer;
|
||||
if (!computedConsumer) {
|
||||
return [];
|
||||
const ruleType = useMemo(() => {
|
||||
if (!computedRuleTypeId || !ruleTypeIndex) {
|
||||
return null;
|
||||
}
|
||||
return getAvailableRuleTypes({
|
||||
consumer: computedConsumer,
|
||||
ruleTypes: [...ruleTypeIndex.values()],
|
||||
ruleTypeRegistry,
|
||||
validConsumers,
|
||||
});
|
||||
}, [consumer, ruleTypeIndex, ruleTypeRegistry, validConsumers, fetchedFormData]);
|
||||
return ruleTypeIndex.get(computedRuleTypeId);
|
||||
}, [computedRuleTypeId, ruleTypeIndex]);
|
||||
|
||||
const [ruleType, ruleTypeModel] = useMemo(() => {
|
||||
const item = authorizedRuleTypeItems.find(({ ruleType: rt }) => {
|
||||
return rt.id === computedRuleTypeId;
|
||||
});
|
||||
|
||||
return [item?.ruleType, item?.ruleTypeModel];
|
||||
}, [authorizedRuleTypeItems, computedRuleTypeId]);
|
||||
const ruleTypeModel = useMemo(() => {
|
||||
if (!computedRuleTypeId) {
|
||||
return null;
|
||||
}
|
||||
return ruleTypeRegistry.get(computedRuleTypeId);
|
||||
}, [computedRuleTypeId, ruleTypeRegistry]);
|
||||
|
||||
const isLoading = useMemo(() => {
|
||||
// Create Mode
|
||||
|
@ -227,6 +219,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
|
|||
isInitialLoading: !!isInitialLoading,
|
||||
ruleType,
|
||||
ruleTypeModel,
|
||||
ruleTypes: [...ruleTypeIndex.values()],
|
||||
uiConfig,
|
||||
healthCheckError,
|
||||
fetchedFormData,
|
||||
|
|
|
@ -117,12 +117,18 @@ describe('ruleActions', () => {
|
|||
getActionTypeModel('1', {
|
||||
id: 'actionType-1',
|
||||
validateParams: mockValidate,
|
||||
defaultActionParams: {
|
||||
key: 'value',
|
||||
},
|
||||
})
|
||||
);
|
||||
actionTypeRegistry.register(
|
||||
getActionTypeModel('2', {
|
||||
id: 'actionType-2',
|
||||
validateParams: mockValidate,
|
||||
defaultActionParams: {
|
||||
key: 'value',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -150,6 +156,10 @@ describe('ruleActions', () => {
|
|||
selectedRuleType: {
|
||||
id: 'selectedRuleTypeId',
|
||||
defaultActionGroupId: 'test',
|
||||
recoveryActionGroup: {
|
||||
id: 'test-recovery-group-id',
|
||||
name: 'test-recovery-group',
|
||||
},
|
||||
producer: 'stackAlerts',
|
||||
},
|
||||
connectors: mockConnectors,
|
||||
|
@ -222,7 +232,7 @@ describe('ruleActions', () => {
|
|||
frequency: { notifyWhen: 'onActionGroupChange', summary: false, throttle: null },
|
||||
group: 'test',
|
||||
id: 'connector-1',
|
||||
params: {},
|
||||
params: { key: 'value' },
|
||||
uuid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
||||
},
|
||||
type: 'addAction',
|
||||
|
|
|
@ -18,6 +18,7 @@ import { ActionConnector, RuleAction, RuleFormParamsErrors } from '../../common/
|
|||
import { DEFAULT_FREQUENCY, MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants';
|
||||
import { RuleActionsItem } from './rule_actions_item';
|
||||
import { RuleActionsSystemActionsItem } from './rule_actions_system_actions_item';
|
||||
import { getDefaultParams } from '../utils';
|
||||
|
||||
export const RuleActions = () => {
|
||||
const [isConnectorModalOpen, setIsConnectorModalOpen] = useState<boolean>(false);
|
||||
|
@ -44,7 +45,15 @@ export const RuleActions = () => {
|
|||
async (connector: ActionConnector) => {
|
||||
const { id, actionTypeId } = connector;
|
||||
const uuid = uuidv4();
|
||||
const params = {};
|
||||
const group = selectedRuleType.defaultActionGroupId;
|
||||
const actionTypeModel = actionTypeRegistry.get(actionTypeId);
|
||||
|
||||
const params =
|
||||
getDefaultParams({
|
||||
group,
|
||||
ruleType: selectedRuleType,
|
||||
actionTypeModel,
|
||||
}) || {};
|
||||
|
||||
dispatch({
|
||||
type: 'addAction',
|
||||
|
@ -53,7 +62,7 @@ export const RuleActions = () => {
|
|||
actionTypeId,
|
||||
uuid,
|
||||
params,
|
||||
group: selectedRuleType.defaultActionGroupId,
|
||||
group,
|
||||
frequency: DEFAULT_FREQUENCY,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -68,6 +68,7 @@ export const RuleActionsAlertsFilter = ({
|
|||
() => onChange(state ? undefined : query),
|
||||
[state, query, onChange]
|
||||
);
|
||||
|
||||
const updateQuery = useCallback(
|
||||
(update: Partial<AlertsFilter['query']>) => {
|
||||
setQuery({
|
||||
|
|
|
@ -163,7 +163,10 @@ export const RuleActionsConnectorsModal = (props: RuleActionsConnectorsModalProp
|
|||
|
||||
const connectorFacetButtons = useMemo(() => {
|
||||
return (
|
||||
<EuiFacetGroup data-test-subj="ruleActionsConnectorsModalFilterButtonGroup">
|
||||
<EuiFacetGroup
|
||||
data-test-subj="ruleActionsConnectorsModalFilterButtonGroup"
|
||||
style={{ overflow: 'auto' }}
|
||||
>
|
||||
<EuiFacetButton
|
||||
data-test-subj="ruleActionsConnectorsModalFilterButton"
|
||||
key="all"
|
||||
|
|
|
@ -324,7 +324,7 @@ describe('ruleActionsItem', () => {
|
|||
|
||||
await userEvent.click(screen.getByText('onTimeframeChange'));
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: {
|
||||
|
|
|
@ -40,17 +40,12 @@ import { isEmpty, some } from 'lodash';
|
|||
import { css } from '@emotion/react';
|
||||
import { SavedObjectAttribute } from '@kbn/core/types';
|
||||
import { useRuleFormDispatch, useRuleFormState } from '../hooks';
|
||||
import {
|
||||
ActionConnector,
|
||||
ActionTypeModel,
|
||||
RuleFormParamsErrors,
|
||||
RuleTypeWithDescription,
|
||||
} from '../../common/types';
|
||||
import { ActionConnector, RuleFormParamsErrors } from '../../common/types';
|
||||
import { getAvailableActionVariables } from '../../action_variables';
|
||||
import { validateAction, validateParamsForWarnings } from '../validation';
|
||||
|
||||
import { RuleActionsSettings } from './rule_actions_settings';
|
||||
import { getSelectedActionGroup } from '../utils';
|
||||
import { getDefaultParams, getSelectedActionGroup } from '../utils';
|
||||
import { RuleActionsMessage } from './rule_actions_message';
|
||||
import {
|
||||
ACTION_ERROR_TOOLTIP,
|
||||
|
@ -60,6 +55,7 @@ import {
|
|||
TECH_PREVIEW_DESCRIPTION,
|
||||
TECH_PREVIEW_LABEL,
|
||||
} from '../translations';
|
||||
import { checkActionFormActionTypeEnabled } from '../utils/check_action_type_enabled';
|
||||
|
||||
const SUMMARY_GROUP_TITLE = i18n.translate('alertsUIShared.ruleActionsItem.summaryGroupTitle', {
|
||||
defaultMessage: 'Summary of alerts',
|
||||
|
@ -83,22 +79,6 @@ const ACTION_TITLE = (connector: ActionConnector) =>
|
|||
},
|
||||
});
|
||||
|
||||
const getDefaultParams = ({
|
||||
group,
|
||||
ruleType,
|
||||
actionTypeModel,
|
||||
}: {
|
||||
group: string;
|
||||
actionTypeModel: ActionTypeModel;
|
||||
ruleType: RuleTypeWithDescription;
|
||||
}) => {
|
||||
if (group === ruleType.recoveryActionGroup.id) {
|
||||
return actionTypeModel.defaultRecoveredActionParams;
|
||||
} else {
|
||||
return actionTypeModel.defaultActionParams;
|
||||
}
|
||||
};
|
||||
|
||||
export interface RuleActionsItemProps {
|
||||
action: RuleAction;
|
||||
index: number;
|
||||
|
@ -178,6 +158,16 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => {
|
|||
? aadTemplateFields
|
||||
: availableActionVariables;
|
||||
|
||||
const checkEnabledResult = useMemo(() => {
|
||||
if (!actionType) {
|
||||
return null;
|
||||
}
|
||||
return checkActionFormActionTypeEnabled(
|
||||
actionType,
|
||||
connectors.filter((c) => c.isPreconfigured)
|
||||
);
|
||||
}, [actionType, connectors]);
|
||||
|
||||
const onDelete = (id: string) => {
|
||||
dispatch({ type: 'removeAction', payload: { uuid: id } });
|
||||
};
|
||||
|
@ -381,16 +371,24 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => {
|
|||
...action.alertsFilter,
|
||||
query,
|
||||
};
|
||||
|
||||
if (!newAlertsFilter.query) {
|
||||
delete newAlertsFilter.query;
|
||||
}
|
||||
|
||||
const alertsFilter = isEmpty(newAlertsFilter) ? undefined : newAlertsFilter;
|
||||
|
||||
const newAction = {
|
||||
...action,
|
||||
alertsFilter: newAlertsFilter,
|
||||
alertsFilter,
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: 'setActionProperty',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
key: 'alertsFilter',
|
||||
value: newAlertsFilter,
|
||||
value: alertsFilter,
|
||||
},
|
||||
});
|
||||
validateActionBase(newAction);
|
||||
|
@ -400,19 +398,33 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => {
|
|||
|
||||
const onTimeframeChange = useCallback(
|
||||
(timeframe?: AlertsFilterTimeframe) => {
|
||||
const newAlertsFilter = {
|
||||
...action.alertsFilter,
|
||||
timeframe,
|
||||
};
|
||||
|
||||
if (!newAlertsFilter.timeframe) {
|
||||
delete newAlertsFilter.timeframe;
|
||||
}
|
||||
|
||||
const alertsFilter = isEmpty(newAlertsFilter) ? undefined : newAlertsFilter;
|
||||
|
||||
const newAction = {
|
||||
...action,
|
||||
alertsFilter,
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: 'setActionProperty',
|
||||
payload: {
|
||||
uuid: action.uuid!,
|
||||
key: 'alertsFilter',
|
||||
value: {
|
||||
...action.alertsFilter,
|
||||
timeframe,
|
||||
},
|
||||
value: alertsFilter,
|
||||
},
|
||||
});
|
||||
validateActionBase(newAction);
|
||||
},
|
||||
[action, dispatch]
|
||||
[action, dispatch, validateActionBase]
|
||||
);
|
||||
|
||||
const onUseAadTemplateFieldsChange = useCallback(() => {
|
||||
|
@ -443,9 +455,25 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => {
|
|||
}, [action, storedActionParamsForAadToggle, dispatch]);
|
||||
|
||||
const accordionContent = useMemo(() => {
|
||||
if (!connector) {
|
||||
if (!connector || !checkEnabledResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!checkEnabledResult.isEnabled) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
style={{
|
||||
padding: euiTheme.size.l,
|
||||
backgroundColor: plain,
|
||||
borderRadius: euiTheme.border.radius.medium,
|
||||
}}
|
||||
>
|
||||
<EuiFlexItem>{checkEnabledResult.messageCard}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
|
@ -504,6 +532,7 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => {
|
|||
templateFields,
|
||||
useDefaultMessage,
|
||||
warning,
|
||||
checkEnabledResult,
|
||||
onNotifyWhenChange,
|
||||
onActionGroupChange,
|
||||
onAlertsFilterChange,
|
||||
|
|
|
@ -74,17 +74,14 @@ describe('RuleAlertDelay', () => {
|
|||
expect(mockOnChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Should call onChange with null if empty string is typed', () => {
|
||||
test('Should not call onChange if empty string is typed', () => {
|
||||
render(<RuleAlertDelay />);
|
||||
fireEvent.change(screen.getByTestId('alertDelayInput'), {
|
||||
target: {
|
||||
value: '',
|
||||
},
|
||||
});
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
type: 'setAlertDelay',
|
||||
payload: null,
|
||||
});
|
||||
expect(mockOnChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Should display error when input is invalid', () => {
|
||||
|
|
|
@ -28,16 +28,8 @@ export const RuleAlertDelay = () => {
|
|||
|
||||
const onAlertDelayChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.validity.valid) {
|
||||
return;
|
||||
}
|
||||
const value = e.target.value;
|
||||
if (value === '') {
|
||||
dispatch({
|
||||
type: 'setAlertDelay',
|
||||
payload: null,
|
||||
});
|
||||
} else if (INTEGER_REGEX.test(value)) {
|
||||
const value = e.target.value.trim();
|
||||
if (INTEGER_REGEX.test(value)) {
|
||||
const parsedValue = parseInt(value, 10);
|
||||
dispatch({
|
||||
type: 'setAlertDelay',
|
||||
|
@ -66,7 +58,7 @@ export const RuleAlertDelay = () => {
|
|||
<EuiFieldNumber
|
||||
fullWidth
|
||||
min={1}
|
||||
value={alertDelay?.active ?? ''}
|
||||
value={alertDelay?.active ?? 1}
|
||||
name="alertDelay"
|
||||
data-test-subj="alertDelayInput"
|
||||
prepend={[ALERT_DELAY_TITLE_PREFIX]}
|
||||
|
|
|
@ -68,7 +68,7 @@ const ruleType = {
|
|||
recoveryActionGroup: 'recovered',
|
||||
producer: 'logs',
|
||||
authorizedConsumers: {
|
||||
alerting: { read: true, all: true },
|
||||
alerts: { read: true, all: true },
|
||||
test: { read: true, all: true },
|
||||
stackAlerts: { read: true, all: true },
|
||||
logs: { read: true, all: true },
|
||||
|
@ -101,6 +101,7 @@ const plugins = {
|
|||
application: {
|
||||
capabilities: {
|
||||
rulesSettings: {
|
||||
readFlappingSettingsUI: true,
|
||||
writeFlappingSettingsUI: true,
|
||||
},
|
||||
},
|
||||
|
@ -133,12 +134,14 @@ describe('Rule Definition', () => {
|
|||
active: 5,
|
||||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
consumer: 'alerts',
|
||||
ruleTypeId: '.es-query',
|
||||
},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
canShowConsumerSelection: true,
|
||||
validConsumers: ['logs', 'stackAlerts'],
|
||||
availableRuleTypes: [ruleType],
|
||||
});
|
||||
|
||||
render(<RuleDefinition />);
|
||||
|
@ -164,13 +167,16 @@ describe('Rule Definition', () => {
|
|||
active: 5,
|
||||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
consumer: 'alerts',
|
||||
ruleTypeId: '.es-query',
|
||||
},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: {
|
||||
...ruleModel,
|
||||
documentationUrl: null,
|
||||
},
|
||||
availableRuleTypes: [ruleType],
|
||||
validConsumers: ['logs', 'stackAlerts'],
|
||||
});
|
||||
render(<RuleDefinition />);
|
||||
|
||||
|
@ -191,6 +197,7 @@ describe('Rule Definition', () => {
|
|||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
ruleTypeId: '.es-query',
|
||||
},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
|
@ -215,9 +222,11 @@ describe('Rule Definition', () => {
|
|||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
ruleTypeId: '.es-query',
|
||||
},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
availableRuleTypes: [ruleType],
|
||||
canShowConsumerSelect: true,
|
||||
validConsumers: ['logs'],
|
||||
});
|
||||
|
@ -241,9 +250,11 @@ describe('Rule Definition', () => {
|
|||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
ruleTypeId: '.es-query',
|
||||
},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
availableRuleTypes: [ruleType],
|
||||
canShowConsumerSelect: true,
|
||||
validConsumers: ['logs', 'observability'],
|
||||
});
|
||||
|
@ -267,9 +278,11 @@ describe('Rule Definition', () => {
|
|||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
ruleTypeId: '.es-query',
|
||||
},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
availableRuleTypes: [ruleType],
|
||||
});
|
||||
|
||||
render(<RuleDefinition />);
|
||||
|
@ -292,9 +305,11 @@ describe('Rule Definition', () => {
|
|||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
ruleTypeId: '.es-query',
|
||||
},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
availableRuleTypes: [ruleType],
|
||||
});
|
||||
|
||||
render(<RuleDefinition />);
|
||||
|
@ -326,9 +341,11 @@ describe('Rule Definition', () => {
|
|||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
ruleTypeId: '.es-query',
|
||||
},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
availableRuleTypes: [ruleType],
|
||||
canShowConsumerSelection: true,
|
||||
validConsumers: ['logs', 'stackAlerts'],
|
||||
});
|
||||
|
@ -339,6 +356,48 @@ describe('Rule Definition', () => {
|
|||
expect(screen.getByTestId('ruleSettingsFlappingForm')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should hide flapping if the user does not have read access', async () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
charts: {} as ChartsPluginSetup,
|
||||
data: {} as DataPublicPluginStart,
|
||||
dataViews: {} as DataViewsPublicPluginStart,
|
||||
unifiedSearch: {} as UnifiedSearchPublicPluginStart,
|
||||
docLinks: {} as DocLinksStart,
|
||||
application: {
|
||||
capabilities: {
|
||||
rulesSettings: {
|
||||
readFlappingSettingsUI: false,
|
||||
writeFlappingSettingsUI: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
formData: {
|
||||
id: 'test-id',
|
||||
params: {},
|
||||
schedule: {
|
||||
interval: '1m',
|
||||
},
|
||||
alertDelay: {
|
||||
active: 5,
|
||||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
ruleTypeId: '.es-query',
|
||||
},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
availableRuleTypes: [ruleType],
|
||||
canShowConsumerSelection: true,
|
||||
validConsumers: ['logs', 'stackAlerts'],
|
||||
});
|
||||
|
||||
render(<RuleDefinition />);
|
||||
|
||||
expect(screen.queryByTestId('ruleDefinitionFlappingFormGroup')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should allow flapping to be changed', async () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins,
|
||||
|
@ -353,9 +412,11 @@ describe('Rule Definition', () => {
|
|||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
ruleTypeId: '.es-query',
|
||||
},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
availableRuleTypes: [ruleType],
|
||||
canShowConsumerSelection: true,
|
||||
validConsumers: ['logs', 'stackAlerts'],
|
||||
});
|
||||
|
@ -389,9 +450,11 @@ describe('Rule Definition', () => {
|
|||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
ruleTypeId: '.es-query',
|
||||
},
|
||||
selectedRuleType: ruleType,
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
availableRuleTypes: [ruleType],
|
||||
canShowConsumerSelection: true,
|
||||
validConsumers: ['logs', 'stackAlerts'],
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { Suspense, useMemo, useState, useCallback } from 'react';
|
||||
import React, { Suspense, useMemo, useState, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiLoadingSpinner,
|
||||
|
@ -47,7 +47,7 @@ import { RuleAlertDelay } from './rule_alert_delay';
|
|||
import { RuleConsumerSelection } from './rule_consumer_selection';
|
||||
import { RuleSchedule } from './rule_schedule';
|
||||
import { useRuleFormState, useRuleFormDispatch } from '../hooks';
|
||||
import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants';
|
||||
import { ALERTING_FEATURE_ID, MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants';
|
||||
import { getAuthorizedConsumers } from '../utils';
|
||||
import { RuleSettingsFlappingTitleTooltip } from '../../rule_settings/rule_settings_flapping_title_tooltip';
|
||||
import { RuleSettingsFlappingForm } from '../../rule_settings/rule_settings_flapping_form';
|
||||
|
@ -62,6 +62,7 @@ export const RuleDefinition = () => {
|
|||
metadata,
|
||||
selectedRuleType,
|
||||
selectedRuleTypeModel,
|
||||
availableRuleTypes,
|
||||
validConsumers,
|
||||
canShowConsumerSelection = false,
|
||||
flappingSettings,
|
||||
|
@ -70,29 +71,44 @@ export const RuleDefinition = () => {
|
|||
const { colorMode } = useEuiTheme();
|
||||
const dispatch = useRuleFormDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
// Need to do a dry run validating the params because the Missing Monitor Data rule type
|
||||
// does not properly initialize the params
|
||||
if (selectedRuleType.id === 'monitoring_alert_missing_monitoring_data') {
|
||||
dispatch({ type: 'runValidation' });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const { charts, data, dataViews, unifiedSearch, docLinks, application } = plugins;
|
||||
|
||||
const {
|
||||
capabilities: { rulesSettings },
|
||||
} = application;
|
||||
|
||||
const { writeFlappingSettingsUI } = rulesSettings || {};
|
||||
const { readFlappingSettingsUI, writeFlappingSettingsUI } = rulesSettings || {};
|
||||
|
||||
const { params, schedule, notifyWhen, flapping } = formData;
|
||||
const { params, schedule, notifyWhen, flapping, consumer, ruleTypeId } = formData;
|
||||
|
||||
const [isAdvancedOptionsVisible, setIsAdvancedOptionsVisible] = useState<boolean>(false);
|
||||
|
||||
const [isFlappingPopoverOpen, setIsFlappingPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
const authorizedConsumers = useMemo(() => {
|
||||
if (!validConsumers?.length) {
|
||||
if (consumer !== ALERTING_FEATURE_ID) {
|
||||
return [];
|
||||
}
|
||||
const selectedAvailableRuleType = availableRuleTypes.find((ruleType) => {
|
||||
return ruleType.id === selectedRuleType.id;
|
||||
});
|
||||
if (!selectedAvailableRuleType?.authorizedConsumers) {
|
||||
return [];
|
||||
}
|
||||
return getAuthorizedConsumers({
|
||||
ruleType: selectedRuleType,
|
||||
ruleType: selectedAvailableRuleType,
|
||||
validConsumers,
|
||||
});
|
||||
}, [selectedRuleType, validConsumers]);
|
||||
}, [consumer, selectedRuleType, availableRuleTypes, validConsumers]);
|
||||
|
||||
const shouldShowConsumerSelect = useMemo(() => {
|
||||
if (!canShowConsumerSelection) {
|
||||
|
@ -107,10 +123,8 @@ export const RuleDefinition = () => {
|
|||
) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
selectedRuleTypeModel.id && MULTI_CONSUMER_RULE_TYPE_IDS.includes(selectedRuleTypeModel.id)
|
||||
);
|
||||
}, [authorizedConsumers, selectedRuleTypeModel, canShowConsumerSelection]);
|
||||
return !!(ruleTypeId && MULTI_CONSUMER_RULE_TYPE_IDS.includes(ruleTypeId));
|
||||
}, [ruleTypeId, authorizedConsumers, canShowConsumerSelection]);
|
||||
|
||||
const RuleParamsExpressionComponent = selectedRuleTypeModel.ruleParamsExpression ?? null;
|
||||
|
||||
|
@ -305,8 +319,9 @@ export const RuleDefinition = () => {
|
|||
>
|
||||
<RuleAlertDelay />
|
||||
</EuiDescribedFormGroup>
|
||||
{IS_RULE_SPECIFIC_FLAPPING_ENABLED && (
|
||||
{IS_RULE_SPECIFIC_FLAPPING_ENABLED && readFlappingSettingsUI && (
|
||||
<EuiDescribedFormGroup
|
||||
data-test-subj="ruleDefinitionFlappingFormGroup"
|
||||
fullWidth
|
||||
title={<h4>{ALERT_FLAPPING_DETECTION_TITLE}</h4>}
|
||||
description={
|
||||
|
|
|
@ -80,9 +80,6 @@ export const RuleSchedule = () => {
|
|||
|
||||
const onIntervalNumberChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.validity.valid) {
|
||||
return;
|
||||
}
|
||||
const value = e.target.value.trim();
|
||||
if (INTEGER_REGEX.test(value)) {
|
||||
const parsedValue = parseInt(value, 10);
|
||||
|
|
|
@ -23,11 +23,12 @@ const queryClient = new QueryClient();
|
|||
|
||||
export interface RuleFormProps {
|
||||
plugins: RuleFormPlugins;
|
||||
returnUrl: string;
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (ruleId: string) => void;
|
||||
}
|
||||
|
||||
export const RuleForm = (props: RuleFormProps) => {
|
||||
const { plugins, returnUrl } = props;
|
||||
const { plugins, onCancel, onSubmit } = props;
|
||||
const { id, ruleTypeId } = useParams<{
|
||||
id?: string;
|
||||
ruleTypeId?: string;
|
||||
|
@ -35,23 +36,31 @@ export const RuleForm = (props: RuleFormProps) => {
|
|||
|
||||
const ruleFormComponent = useMemo(() => {
|
||||
if (id) {
|
||||
return <EditRuleForm id={id} plugins={plugins} returnUrl={returnUrl} />;
|
||||
return <EditRuleForm id={id} plugins={plugins} onCancel={onCancel} onSubmit={onSubmit} />;
|
||||
}
|
||||
if (ruleTypeId) {
|
||||
return <CreateRuleForm ruleTypeId={ruleTypeId} plugins={plugins} returnUrl={returnUrl} />;
|
||||
return (
|
||||
<CreateRuleForm
|
||||
ruleTypeId={ruleTypeId}
|
||||
plugins={plugins}
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
color="danger"
|
||||
iconType="error"
|
||||
title={<h2>{RULE_FORM_ROUTE_PARAMS_ERROR_TITLE}</h2>}
|
||||
>
|
||||
<EuiText>
|
||||
<p>{RULE_FORM_ROUTE_PARAMS_ERROR_TEXT}</p>
|
||||
</EuiText>
|
||||
</EuiEmptyPrompt>
|
||||
body={
|
||||
<EuiText>
|
||||
<p>{RULE_FORM_ROUTE_PARAMS_ERROR_TEXT}</p>
|
||||
</EuiText>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}, [id, ruleTypeId, plugins, returnUrl]);
|
||||
}, [id, ruleTypeId, plugins, onCancel, onSubmit]);
|
||||
|
||||
return <QueryClientProvider client={queryClient}>{ruleFormComponent}</QueryClientProvider>;
|
||||
};
|
||||
|
|
|
@ -76,6 +76,8 @@ const initialState: RuleFormState = {
|
|||
selectedRuleType: indexThresholdRuleType,
|
||||
selectedRuleTypeModel: indexThresholdRuleTypeModel,
|
||||
multiConsumerSelection: 'stackAlerts',
|
||||
availableRuleTypes: [],
|
||||
validConsumers: [],
|
||||
connectors: [],
|
||||
connectorTypes: [],
|
||||
aadTemplateFields: [],
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { RuleActionParams } from '@kbn/alerting-types';
|
||||
import { omit } from 'lodash';
|
||||
import { isEmpty, omit } from 'lodash';
|
||||
import { RuleFormActionsErrors, RuleFormParamsErrors, RuleUiAction } from '../../common';
|
||||
import { RuleFormData, RuleFormState } from '../types';
|
||||
import { validateRuleBase, validateRuleParams } from '../validation';
|
||||
|
@ -106,13 +106,20 @@ export type RuleFormStateReducerAction =
|
|||
uuid: string;
|
||||
errors: RuleFormParamsErrors;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'runValidation';
|
||||
};
|
||||
|
||||
const getUpdateWithValidation =
|
||||
(ruleFormState: RuleFormState) =>
|
||||
(updater: () => RuleFormData): RuleFormState => {
|
||||
const { minimumScheduleInterval, selectedRuleTypeModel, multiConsumerSelection } =
|
||||
ruleFormState;
|
||||
const {
|
||||
minimumScheduleInterval,
|
||||
selectedRuleTypeModel,
|
||||
multiConsumerSelection,
|
||||
selectedRuleType,
|
||||
} = ruleFormState;
|
||||
|
||||
const formData = updater();
|
||||
|
||||
|
@ -121,17 +128,33 @@ const getUpdateWithValidation =
|
|||
...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}),
|
||||
};
|
||||
|
||||
const baseErrors = validateRuleBase({
|
||||
formData: formDataWithMultiConsumer,
|
||||
minimumScheduleInterval,
|
||||
});
|
||||
|
||||
const paramsErrors = validateRuleParams({
|
||||
formData: formDataWithMultiConsumer,
|
||||
ruleTypeModel: selectedRuleTypeModel,
|
||||
});
|
||||
|
||||
// We need to do this because the Missing Monitor Data rule type
|
||||
// for whatever reason does not initialize the params with any data,
|
||||
// therefore the expression component renders as blank
|
||||
if (selectedRuleType.id === 'monitoring_alert_missing_monitoring_data') {
|
||||
if (isEmpty(formData.params) && !isEmpty(paramsErrors)) {
|
||||
Object.keys(paramsErrors).forEach((key) => {
|
||||
formData.params[key] = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...ruleFormState,
|
||||
formData,
|
||||
baseErrors: validateRuleBase({
|
||||
formData: formDataWithMultiConsumer,
|
||||
minimumScheduleInterval,
|
||||
}),
|
||||
paramsErrors: validateRuleParams({
|
||||
formData: formDataWithMultiConsumer,
|
||||
ruleTypeModel: selectedRuleTypeModel,
|
||||
}),
|
||||
baseErrors,
|
||||
paramsErrors,
|
||||
touched: true,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -222,6 +245,7 @@ export const ruleFormStateReducer = (
|
|||
return {
|
||||
...ruleFormState,
|
||||
multiConsumerSelection: payload,
|
||||
touched: true,
|
||||
};
|
||||
}
|
||||
case 'setMetadata': {
|
||||
|
@ -326,6 +350,9 @@ export const ruleFormStateReducer = (
|
|||
},
|
||||
};
|
||||
}
|
||||
case 'runValidation': {
|
||||
return updateWithValidation(() => formData);
|
||||
}
|
||||
default: {
|
||||
return ruleFormState;
|
||||
}
|
||||
|
|
|
@ -61,6 +61,8 @@ const formDataMock: RuleFormData = {
|
|||
},
|
||||
};
|
||||
|
||||
const onCancel = jest.fn();
|
||||
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
application: {
|
||||
|
@ -84,7 +86,6 @@ useRuleFormState.mockReturnValue({
|
|||
});
|
||||
|
||||
const onSave = jest.fn();
|
||||
const returnUrl = 'management';
|
||||
|
||||
describe('rulePage', () => {
|
||||
afterEach(() => {
|
||||
|
@ -92,7 +93,7 @@ describe('rulePage', () => {
|
|||
});
|
||||
|
||||
test('renders correctly', () => {
|
||||
render(<RulePage returnUrl={returnUrl} onSave={onSave} />);
|
||||
render(<RulePage onCancel={onCancel} onSave={onSave} />);
|
||||
|
||||
expect(screen.getByText(RULE_FORM_PAGE_RULE_DEFINITION_TITLE)).toBeInTheDocument();
|
||||
expect(screen.getByText(RULE_FORM_PAGE_RULE_ACTIONS_TITLE)).toBeInTheDocument();
|
||||
|
@ -100,7 +101,7 @@ describe('rulePage', () => {
|
|||
});
|
||||
|
||||
test('should call onSave when save button is pressed', () => {
|
||||
render(<RulePage returnUrl={returnUrl} onSave={onSave} />);
|
||||
render(<RulePage onCancel={onCancel} onSave={onSave} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('rulePageFooterSaveButton'));
|
||||
fireEvent.click(screen.getByTestId('confirmModalConfirmButton'));
|
||||
|
@ -112,16 +113,16 @@ describe('rulePage', () => {
|
|||
});
|
||||
|
||||
test('should call onCancel when the cancel button is clicked', () => {
|
||||
render(<RulePage returnUrl={returnUrl} onSave={onSave} />);
|
||||
render(<RulePage onCancel={onCancel} onSave={onSave} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('rulePageFooterCancelButton'));
|
||||
expect(navigateToUrl).toHaveBeenCalledWith('management');
|
||||
expect(onCancel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should call onCancel when the return button is clicked', () => {
|
||||
render(<RulePage returnUrl={returnUrl} onSave={onSave} />);
|
||||
render(<RulePage onCancel={onCancel} onSave={onSave} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('rulePageReturnButton'));
|
||||
expect(navigateToUrl).toHaveBeenCalledWith('management');
|
||||
expect(onCancel).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiPageTemplate,
|
||||
EuiHorizontalRule,
|
||||
|
@ -18,6 +18,8 @@ import {
|
|||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiCallOut,
|
||||
EuiConfirmModal,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
RuleDefinition,
|
||||
|
@ -33,32 +35,45 @@ import {
|
|||
RULE_FORM_PAGE_RULE_ACTIONS_TITLE,
|
||||
RULE_FORM_PAGE_RULE_DETAILS_TITLE,
|
||||
RULE_FORM_RETURN_TITLE,
|
||||
DISABLED_ACTIONS_WARNING_TITLE,
|
||||
RULE_FORM_CANCEL_MODAL_TITLE,
|
||||
RULE_FORM_CANCEL_MODAL_DESCRIPTION,
|
||||
RULE_FORM_CANCEL_MODAL_CONFIRM,
|
||||
RULE_FORM_CANCEL_MODAL_CANCEL,
|
||||
} from '../translations';
|
||||
import { hasActionsError, hasActionsParamsErrors, hasParamsErrors } from '../validation';
|
||||
import { checkActionFormActionTypeEnabled } from '../utils/check_action_type_enabled';
|
||||
|
||||
export interface RulePageProps {
|
||||
isEdit?: boolean;
|
||||
isSaving?: boolean;
|
||||
returnUrl: string;
|
||||
onCancel?: () => void;
|
||||
onSave: (formData: RuleFormData) => void;
|
||||
}
|
||||
|
||||
export const RulePage = (props: RulePageProps) => {
|
||||
const { isEdit = false, isSaving = false, returnUrl, onSave } = props;
|
||||
const { isEdit = false, isSaving = false, onCancel = () => {}, onSave } = props;
|
||||
const [isCancelModalOpen, setIsCancelModalOpen] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
plugins: { application },
|
||||
baseErrors = {},
|
||||
paramsErrors = {},
|
||||
actionsErrors = {},
|
||||
actionsParamsErrors = {},
|
||||
formData,
|
||||
multiConsumerSelection,
|
||||
connectorTypes,
|
||||
connectors,
|
||||
touched,
|
||||
} = useRuleFormState();
|
||||
|
||||
const { actions } = formData;
|
||||
|
||||
const canReadConnectors = !!application.capabilities.actions?.show;
|
||||
|
||||
const styles = useEuiBackgroundColorCSS().transparent;
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
application.navigateToUrl(returnUrl);
|
||||
}, [application, returnUrl]);
|
||||
|
||||
const onSaveInternal = useCallback(() => {
|
||||
onSave({
|
||||
...formData,
|
||||
|
@ -66,11 +81,51 @@ export const RulePage = (props: RulePageProps) => {
|
|||
});
|
||||
}, [onSave, formData, multiConsumerSelection]);
|
||||
|
||||
const actionComponent = useMemo(() => {
|
||||
const onCancelInternal = useCallback(() => {
|
||||
if (touched) {
|
||||
setIsCancelModalOpen(true);
|
||||
} else {
|
||||
onCancel();
|
||||
}
|
||||
}, [touched, onCancel]);
|
||||
|
||||
const hasActionsDisabled = useMemo(() => {
|
||||
const preconfiguredConnectors = connectors.filter((connector) => connector.isPreconfigured);
|
||||
return actions.some((action) => {
|
||||
const actionType = connectorTypes.find(({ id }) => id === action.actionTypeId);
|
||||
if (!actionType) {
|
||||
return false;
|
||||
}
|
||||
const checkEnabledResult = checkActionFormActionTypeEnabled(
|
||||
actionType,
|
||||
preconfiguredConnectors
|
||||
);
|
||||
return !actionType.enabled && !checkEnabledResult.isEnabled;
|
||||
});
|
||||
}, [actions, connectors, connectorTypes]);
|
||||
|
||||
const hasRuleDefinitionErrors = useMemo(() => {
|
||||
return !!(
|
||||
hasParamsErrors(paramsErrors) ||
|
||||
baseErrors.interval?.length ||
|
||||
baseErrors.alertDelay?.length
|
||||
);
|
||||
}, [paramsErrors, baseErrors]);
|
||||
|
||||
const hasActionErrors = useMemo(() => {
|
||||
return hasActionsError(actionsErrors) || hasActionsParamsErrors(actionsParamsErrors);
|
||||
}, [actionsErrors, actionsParamsErrors]);
|
||||
|
||||
const hasRuleDetailsError = useMemo(() => {
|
||||
return baseErrors.name?.length || baseErrors.tags?.length;
|
||||
}, [baseErrors]);
|
||||
|
||||
const actionComponent: EuiStepsProps['steps'] = useMemo(() => {
|
||||
if (canReadConnectors) {
|
||||
return [
|
||||
{
|
||||
title: RULE_FORM_PAGE_RULE_ACTIONS_TITLE,
|
||||
status: hasActionErrors ? 'danger' : undefined,
|
||||
children: (
|
||||
<>
|
||||
<RuleActions />
|
||||
|
@ -82,17 +137,19 @@ export const RulePage = (props: RulePageProps) => {
|
|||
];
|
||||
}
|
||||
return [];
|
||||
}, [canReadConnectors]);
|
||||
}, [hasActionErrors, canReadConnectors]);
|
||||
|
||||
const steps: EuiStepsProps['steps'] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: RULE_FORM_PAGE_RULE_DEFINITION_TITLE,
|
||||
status: hasRuleDefinitionErrors ? 'danger' : undefined,
|
||||
children: <RuleDefinition />,
|
||||
},
|
||||
...actionComponent,
|
||||
{
|
||||
title: RULE_FORM_PAGE_RULE_DETAILS_TITLE,
|
||||
status: hasRuleDetailsError ? 'danger' : undefined,
|
||||
children: (
|
||||
<>
|
||||
<RuleDetails />
|
||||
|
@ -102,46 +159,73 @@ export const RulePage = (props: RulePageProps) => {
|
|||
),
|
||||
},
|
||||
];
|
||||
}, [actionComponent]);
|
||||
}, [hasRuleDefinitionErrors, hasRuleDetailsError, actionComponent]);
|
||||
|
||||
return (
|
||||
<EuiPageTemplate grow bottomBorder offset={0} css={styles}>
|
||||
<EuiPageTemplate.Header>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
alignItems="flexStart"
|
||||
className="eui-fullWidth"
|
||||
<>
|
||||
<EuiPageTemplate grow bottomBorder offset={0} css={styles}>
|
||||
<EuiPageTemplate.Header>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
alignItems="flexStart"
|
||||
className="eui-fullWidth"
|
||||
>
|
||||
<EuiFlexItem grow={false} style={{ alignItems: 'start' }}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="rulePageReturnButton"
|
||||
onClick={onCancelInternal}
|
||||
style={{ padding: 0 }}
|
||||
iconType="arrowLeft"
|
||||
iconSide="left"
|
||||
aria-label="Return link"
|
||||
>
|
||||
{RULE_FORM_RETURN_TITLE}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer />
|
||||
<EuiFlexItem grow={false} className="eui-fullWidth">
|
||||
<RulePageNameInput />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageTemplate.Header>
|
||||
<EuiPageTemplate.Section>
|
||||
{hasActionsDisabled && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
color="danger"
|
||||
iconType="error"
|
||||
data-test-subj="hasActionsDisabled"
|
||||
title={DISABLED_ACTIONS_WARNING_TITLE}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
<EuiSteps steps={steps} />
|
||||
</EuiPageTemplate.Section>
|
||||
<EuiPageTemplate.Section>
|
||||
<RulePageFooter
|
||||
isEdit={isEdit}
|
||||
isSaving={isSaving}
|
||||
onCancel={onCancelInternal}
|
||||
onSave={onSaveInternal}
|
||||
/>
|
||||
</EuiPageTemplate.Section>
|
||||
</EuiPageTemplate>
|
||||
{isCancelModalOpen && (
|
||||
<EuiConfirmModal
|
||||
onCancel={() => setIsCancelModalOpen(false)}
|
||||
onConfirm={onCancel}
|
||||
buttonColor="danger"
|
||||
defaultFocusedButton="confirm"
|
||||
title={RULE_FORM_CANCEL_MODAL_TITLE}
|
||||
confirmButtonText={RULE_FORM_CANCEL_MODAL_CONFIRM}
|
||||
cancelButtonText={RULE_FORM_CANCEL_MODAL_CANCEL}
|
||||
>
|
||||
<EuiFlexItem grow={false} style={{ alignItems: 'start' }}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="rulePageReturnButton"
|
||||
onClick={onCancel}
|
||||
style={{ padding: 0 }}
|
||||
iconType="arrowLeft"
|
||||
iconSide="left"
|
||||
aria-label="Return link"
|
||||
>
|
||||
{RULE_FORM_RETURN_TITLE}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer />
|
||||
<EuiFlexItem grow={false} className="eui-fullWidth">
|
||||
<RulePageNameInput />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageTemplate.Header>
|
||||
<EuiPageTemplate.Section>
|
||||
<EuiSteps steps={steps} />
|
||||
</EuiPageTemplate.Section>
|
||||
<EuiPageTemplate.Section>
|
||||
<RulePageFooter
|
||||
isEdit={isEdit}
|
||||
isSaving={isSaving}
|
||||
onCancel={onCancel}
|
||||
onSave={onSaveInternal}
|
||||
/>
|
||||
</EuiPageTemplate.Section>
|
||||
</EuiPageTemplate>
|
||||
<p>{RULE_FORM_CANCEL_MODAL_DESCRIPTION}</p>
|
||||
</EuiConfirmModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -32,15 +32,27 @@ const onSave = jest.fn();
|
|||
const onCancel = jest.fn();
|
||||
|
||||
hasRuleErrors.mockReturnValue(false);
|
||||
useRuleFormState.mockReturnValue({
|
||||
baseErrors: {},
|
||||
paramsErrors: {},
|
||||
formData: {
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
|
||||
describe('rulePageFooter', () => {
|
||||
beforeEach(() => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
application: {
|
||||
capabilities: {
|
||||
actions: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
baseErrors: {},
|
||||
paramsErrors: {},
|
||||
formData: {
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
@ -75,6 +87,30 @@ describe('rulePageFooter', () => {
|
|||
expect(screen.getByTestId('rulePageConfirmCreateRule')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not show creat rule confirmation if user cannot read actions', () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
application: {
|
||||
capabilities: {
|
||||
actions: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
baseErrors: {},
|
||||
paramsErrors: {},
|
||||
formData: {
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
|
||||
render(<RulePageFooter onSave={onSave} onCancel={onCancel} />);
|
||||
fireEvent.click(screen.getByTestId('rulePageFooterSaveButton'));
|
||||
expect(screen.queryByTestId('rulePageConfirmCreateRule')).not.toBeInTheDocument();
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should show call onSave if clicking rule confirmation', () => {
|
||||
render(<RulePageFooter onSave={onSave} onCancel={onCancel} />);
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ export const RulePageFooter = (props: RulePageFooterProps) => {
|
|||
const { isEdit = false, isSaving = false, onCancel, onSave } = props;
|
||||
|
||||
const {
|
||||
plugins: { application },
|
||||
formData: { actions },
|
||||
connectors,
|
||||
baseErrors = {},
|
||||
|
@ -78,11 +79,12 @@ export const RulePageFooter = (props: RulePageFooterProps) => {
|
|||
if (isEdit) {
|
||||
return onSave();
|
||||
}
|
||||
if (actions.length === 0) {
|
||||
const canReadConnectors = !!application.capabilities.actions?.show;
|
||||
if (actions.length === 0 && canReadConnectors) {
|
||||
return setShowCreateConfirmation(true);
|
||||
}
|
||||
onSave();
|
||||
}, [actions, isEdit, onSave]);
|
||||
}, [actions, isEdit, application, onSave]);
|
||||
|
||||
const onCreateConfirmClick = useCallback(() => {
|
||||
setShowCreateConfirmation(false);
|
||||
|
|
|
@ -194,7 +194,7 @@ export const RULE_TYPE_REQUIRED_TEXT = i18n.translate(
|
|||
export const RULE_ALERT_DELAY_BELOW_MINIMUM_TEXT = i18n.translate(
|
||||
'alertsUIShared.ruleForm.error.belowMinimumAlertDelayText',
|
||||
{
|
||||
defaultMessage: 'Alert delay must be greater than 1.',
|
||||
defaultMessage: 'Alert delay must be 1 or greater.',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -498,6 +498,34 @@ export const RULE_FORM_RETURN_TITLE = i18n.translate('alertsUIShared.ruleForm.re
|
|||
defaultMessage: 'Return',
|
||||
});
|
||||
|
||||
export const RULE_FORM_CANCEL_MODAL_TITLE = i18n.translate(
|
||||
'alertsUIShared.ruleForm.ruleFormCancelModalTitle',
|
||||
{
|
||||
defaultMessage: 'Discard unsaved changes to rule?',
|
||||
}
|
||||
);
|
||||
|
||||
export const RULE_FORM_CANCEL_MODAL_DESCRIPTION = i18n.translate(
|
||||
'alertsUIShared.ruleForm.ruleFormCancelModalDescription',
|
||||
{
|
||||
defaultMessage: "You can't recover unsaved changes.",
|
||||
}
|
||||
);
|
||||
|
||||
export const RULE_FORM_CANCEL_MODAL_CONFIRM = i18n.translate(
|
||||
'alertsUIShared.ruleForm.ruleFormCancelModalConfirm',
|
||||
{
|
||||
defaultMessage: 'Discard changes',
|
||||
}
|
||||
);
|
||||
|
||||
export const RULE_FORM_CANCEL_MODAL_CANCEL = i18n.translate(
|
||||
'alertsUIShared.ruleForm.ruleFormCancelModalCancel',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
);
|
||||
|
||||
export const MODAL_SEARCH_PLACEHOLDER = i18n.translate(
|
||||
'alertsUIShared.ruleForm.modalSearchPlaceholder',
|
||||
{
|
||||
|
@ -586,3 +614,10 @@ export const TECH_PREVIEW_DESCRIPTION = i18n.translate(
|
|||
'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.',
|
||||
}
|
||||
);
|
||||
|
||||
export const DISABLED_ACTIONS_WARNING_TITLE = i18n.translate(
|
||||
'alertsUIShared.disabledActionsWarningTitle',
|
||||
{
|
||||
defaultMessage: 'This rule has actions that are disabled',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -72,6 +72,7 @@ export interface RuleFormState<Params extends RuleTypeParams = RuleTypeParams> {
|
|||
connectors: ActionConnector[];
|
||||
connectorTypes: ActionType[];
|
||||
aadTemplateFields: ActionVariable[];
|
||||
availableRuleTypes: RuleTypeWithDescription[];
|
||||
baseErrors?: RuleFormBaseErrors;
|
||||
paramsErrors?: RuleFormParamsErrors;
|
||||
actionsErrors?: Record<string, RuleFormActionsErrors>;
|
||||
|
@ -83,8 +84,9 @@ export interface RuleFormState<Params extends RuleTypeParams = RuleTypeParams> {
|
|||
metadata?: Record<string, unknown>;
|
||||
minimumScheduleInterval?: MinimumScheduleInterval;
|
||||
canShowConsumerSelection?: boolean;
|
||||
validConsumers?: RuleCreationValidConsumer[];
|
||||
validConsumers: RuleCreationValidConsumer[];
|
||||
flappingSettings?: RulesSettingsFlapping;
|
||||
touched?: boolean;
|
||||
}
|
||||
|
||||
export type InitialRule = Partial<Rule> &
|
||||
|
|
|
@ -17,9 +17,6 @@ export const getAuthorizedConsumers = ({
|
|||
ruleType: RuleTypeWithDescription;
|
||||
validConsumers: RuleCreationValidConsumer[];
|
||||
}) => {
|
||||
if (!ruleType.authorizedConsumers) {
|
||||
return [];
|
||||
}
|
||||
return Object.entries(ruleType.authorizedConsumers).reduce<RuleCreationValidConsumer[]>(
|
||||
(result, [authorizedConsumer, privilege]) => {
|
||||
if (
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
import { ActionTypeModel, RuleTypeWithDescription } from '../../common/types';
|
||||
|
||||
export const getDefaultParams = ({
|
||||
group,
|
||||
ruleType,
|
||||
actionTypeModel,
|
||||
}: {
|
||||
group: string;
|
||||
actionTypeModel: ActionTypeModel;
|
||||
ruleType: RuleTypeWithDescription;
|
||||
}) => {
|
||||
if (group === ruleType.recoveryActionGroup.id) {
|
||||
return actionTypeModel.defaultRecoveredActionParams;
|
||||
} else {
|
||||
return actionTypeModel.defaultActionParams;
|
||||
}
|
||||
};
|
|
@ -17,3 +17,4 @@ export * from './get_initial_schedule';
|
|||
export * from './has_fields_for_aad';
|
||||
export * from './get_selected_action_group';
|
||||
export * from './get_initial_consumer';
|
||||
export * from './get_default_params';
|
||||
|
|
|
@ -35,7 +35,10 @@ export const validateAction = ({ action }: { action: RuleUiAction }): RuleFormAc
|
|||
|
||||
if ('alertsFilter' in action) {
|
||||
const query = action?.alertsFilter?.query;
|
||||
if (query && !query.kql) {
|
||||
if (!query) {
|
||||
return errors;
|
||||
}
|
||||
if (!query.filters.length && !query.kql) {
|
||||
errors.filterQuery.push(
|
||||
i18n.translate('alertsUIShared.ruleForm.actionsForm.requiredFilterQuery', {
|
||||
defaultMessage: 'A custom query is required.',
|
||||
|
@ -43,7 +46,6 @@ export const validateAction = ({ action }: { action: RuleUiAction }): RuleFormAc
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
|
@ -88,11 +90,7 @@ export function validateRuleBase({
|
|||
errors.ruleTypeId.push(RULE_TYPE_REQUIRED_TEXT);
|
||||
}
|
||||
|
||||
if (
|
||||
formData.alertDelay &&
|
||||
!isNaN(formData.alertDelay?.active) &&
|
||||
formData.alertDelay?.active < 1
|
||||
) {
|
||||
if (!formData.alertDelay || isNaN(formData.alertDelay.active) || formData.alertDelay.active < 1) {
|
||||
errors.alertDelay.push(RULE_ALERT_DELAY_BELOW_MINIMUM_TEXT);
|
||||
}
|
||||
|
||||
|
@ -111,34 +109,41 @@ export const validateRuleParams = ({
|
|||
return ruleTypeModel.validate(formData.params, isServerless).errors;
|
||||
};
|
||||
|
||||
const hasRuleBaseErrors = (errors: RuleFormBaseErrors) => {
|
||||
export const hasRuleBaseErrors = (errors: RuleFormBaseErrors) => {
|
||||
return Object.values(errors).some((error: string[]) => error.length > 0);
|
||||
};
|
||||
|
||||
const hasActionsError = (actionsErrors: Record<string, RuleFormActionsErrors>) => {
|
||||
export const hasActionsError = (actionsErrors: Record<string, RuleFormActionsErrors>) => {
|
||||
return Object.values(actionsErrors).some((errors: RuleFormActionsErrors) => {
|
||||
return Object.values(errors).some((error: string[]) => error.length > 0);
|
||||
});
|
||||
};
|
||||
|
||||
const hasParamsErrors = (errors: RuleFormParamsErrors): boolean => {
|
||||
const values = Object.values(errors);
|
||||
export const hasParamsErrors = (errors: RuleFormParamsErrors | string | string[]): boolean => {
|
||||
let hasError = false;
|
||||
for (const value of values) {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (typeof value === 'string' && value.trim() !== '') {
|
||||
return true;
|
||||
}
|
||||
if (isObject(value)) {
|
||||
hasError = hasParamsErrors(value as RuleFormParamsErrors);
|
||||
}
|
||||
|
||||
if (typeof errors === 'string' && errors.trim() !== '') {
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
if (Array.isArray(errors)) {
|
||||
errors.forEach((error) => {
|
||||
hasError = hasError || hasParamsErrors(error);
|
||||
});
|
||||
}
|
||||
|
||||
if (isObject(errors)) {
|
||||
Object.entries(errors).forEach(([_, value]) => {
|
||||
hasError = hasError || hasParamsErrors(value);
|
||||
});
|
||||
}
|
||||
|
||||
return hasError;
|
||||
};
|
||||
|
||||
const hasActionsParamsErrors = (actionsParamsErrors: Record<string, RuleFormParamsErrors>) => {
|
||||
export const hasActionsParamsErrors = (
|
||||
actionsParamsErrors: Record<string, RuleFormParamsErrors>
|
||||
) => {
|
||||
return Object.values(actionsParamsErrors).some((errors: RuleFormParamsErrors) => {
|
||||
return hasParamsErrors(errors);
|
||||
});
|
||||
|
|
|
@ -218,15 +218,17 @@ export const RuleSettingsFlappingForm = (props: RuleSettingsFlappingFormProps) =
|
|||
direction={isDesktop ? 'row' : 'column'}
|
||||
alignItems={isDesktop ? 'center' : undefined}
|
||||
>
|
||||
<EuiFlexItem style={{ flexDirection: 'row' }}>
|
||||
<EuiFlexItem style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<EuiText size="s" style={{ marginRight: euiTheme.size.xs }}>
|
||||
{flappingLabel}
|
||||
</EuiText>
|
||||
<EuiBadge color={enabled ? 'success' : 'default'}>
|
||||
<EuiBadge color={enabled ? 'success' : 'default'} style={{ height: '100%' }}>
|
||||
{enabled ? flappingOnLabel : flappingOffLabel}
|
||||
</EuiBadge>
|
||||
{flappingSettings && enabled && (
|
||||
<EuiBadge color="primary">{flappingOverrideLabel}</EuiBadge>
|
||||
<EuiBadge color="primary" style={{ height: '100%' }}>
|
||||
{flappingOverrideLabel}
|
||||
</EuiBadge>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -236,6 +238,7 @@ export const RuleSettingsFlappingForm = (props: RuleSettingsFlappingFormProps) =
|
|||
compressed
|
||||
checked={!!flappingSettings}
|
||||
label={flappingOverrideConfiguration}
|
||||
disabled={!canWriteFlappingSettingsUI}
|
||||
onChange={onFlappingToggle}
|
||||
/>
|
||||
)}
|
||||
|
@ -256,6 +259,7 @@ export const RuleSettingsFlappingForm = (props: RuleSettingsFlappingFormProps) =
|
|||
spaceFlappingSettings,
|
||||
flappingSettings,
|
||||
flappingOffTooltip,
|
||||
canWriteFlappingSettingsUI,
|
||||
onFlappingToggle,
|
||||
]);
|
||||
|
||||
|
@ -273,12 +277,14 @@ export const RuleSettingsFlappingForm = (props: RuleSettingsFlappingFormProps) =
|
|||
statusChangeThreshold={flappingSettings.statusChangeThreshold}
|
||||
onLookBackWindowChange={onLookBackWindowChange}
|
||||
onStatusChangeThresholdChange={onStatusChangeThresholdChange}
|
||||
isDisabled={!canWriteFlappingSettingsUI}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}, [
|
||||
flappingSettings,
|
||||
spaceFlappingSettings,
|
||||
canWriteFlappingSettingsUI,
|
||||
onLookBackWindowChange,
|
||||
onStatusChangeThresholdChange,
|
||||
]);
|
||||
|
|
|
@ -80,6 +80,7 @@ export const RuleSettingsFlappingTitleTooltip = (props: RuleSettingsFlappingTitl
|
|||
panelStyle={{
|
||||
width: 500,
|
||||
}}
|
||||
closePopover={() => setIsPopoverOpen(false)}
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
data-test-subj="ruleSettingsFlappingTitleTooltipButton"
|
||||
|
|
|
@ -9,5 +9,10 @@
|
|||
|
||||
export const ruleDetailsRoute = '/rule/:ruleId' as const;
|
||||
export const triggersActionsRoute = '/app/management/insightsAndAlerting/triggersActions' as const;
|
||||
export const createRuleRoute = '/rules/create/:ruleTypeId' as const;
|
||||
export const editRuleRoute = '/rules/edit/:id' as const;
|
||||
|
||||
export const getRuleDetailsRoute = (ruleId: string) => ruleDetailsRoute.replace(':ruleId', ruleId);
|
||||
export const getCreateRuleRoute = (ruleTypeId: string) =>
|
||||
createRuleRoute.replace(':ruleTypeId', ruleTypeId);
|
||||
export const getEditRuleRoute = (ruleId: string) => editRuleRoute.replace(':id', ruleId);
|
||||
|
|
|
@ -203,7 +203,6 @@ const TriggersActionsUiExampleApp = ({
|
|||
ruleTypeRegistry: triggersActionsUi.ruleTypeRegistry,
|
||||
actionTypeRegistry: triggersActionsUi.actionTypeRegistry,
|
||||
}}
|
||||
returnUrl={application.getUrlForApp('triggersActionsUiExample')}
|
||||
/>
|
||||
</Page>
|
||||
)}
|
||||
|
@ -229,7 +228,6 @@ const TriggersActionsUiExampleApp = ({
|
|||
ruleTypeRegistry: triggersActionsUi.ruleTypeRegistry,
|
||||
actionTypeRegistry: triggersActionsUi.actionTypeRegistry,
|
||||
}}
|
||||
returnUrl={application.getUrlForApp('triggersActionsUiExample')}
|
||||
/>
|
||||
</Page>
|
||||
)}
|
||||
|
|
|
@ -69,7 +69,7 @@ export const StorybookContextDecorator: FC<PropsWithChildren<StorybookContextDec
|
|||
ruleKqlBar: true,
|
||||
isMustacheAutocompleteOn: false,
|
||||
showMustacheAutocompleteSwitch: false,
|
||||
ruleFormV2: false,
|
||||
isUsingRuleCreateFlyout: false,
|
||||
},
|
||||
});
|
||||
return (
|
||||
|
|
|
@ -21,7 +21,7 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
ruleKqlBar: false,
|
||||
isMustacheAutocompleteOn: false,
|
||||
showMustacheAutocompleteSwitch: false,
|
||||
ruleFormV2: false,
|
||||
isUsingRuleCreateFlyout: false,
|
||||
});
|
||||
|
||||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
|
|
|
@ -25,6 +25,8 @@ export const routeToConnectors = `/connectors`;
|
|||
export const routeToConnectorEdit = `/connectors/:connectorId`;
|
||||
export const routeToRules = `/rules`;
|
||||
export const routeToLogs = `/logs`;
|
||||
export const routeToCreateRule = '/rules/create';
|
||||
export const routeToEditRule = '/rules/edit';
|
||||
export const legacyRouteToAlerts = `/alerts`;
|
||||
export const legacyRouteToRuleDetails = `/alert/:alertId`;
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
|
|||
'xl'
|
||||
)({
|
||||
showCreateRuleButtonInPrompt: true,
|
||||
useNewRuleForm: true,
|
||||
setHeaderActions,
|
||||
});
|
||||
}, []);
|
||||
|
|
|
@ -64,6 +64,18 @@ export const getAlertingSectionBreadcrumb = (
|
|||
}
|
||||
: {}),
|
||||
};
|
||||
case 'createRule':
|
||||
return {
|
||||
text: i18n.translate('xpack.triggersActionsUI.rules.create.breadcrumbTitle', {
|
||||
defaultMessage: 'Create',
|
||||
}),
|
||||
};
|
||||
case 'editRule':
|
||||
return {
|
||||
text: i18n.translate('xpack.triggersActionsUI.rules.edit.breadcrumbTitle', {
|
||||
defaultMessage: 'Edit',
|
||||
}),
|
||||
};
|
||||
default:
|
||||
return {
|
||||
text: i18n.translate('xpack.triggersActionsUI.home.breadcrumbTitle', {
|
||||
|
|
|
@ -26,6 +26,16 @@ export const getCurrentDocTitle = (page: string): string => {
|
|||
defaultMessage: 'Rules',
|
||||
});
|
||||
break;
|
||||
case 'createRule':
|
||||
updatedTitle = i18n.translate('xpack.triggersActionsUI.rules.createRule.breadcrumbTitle', {
|
||||
defaultMessage: 'Create rule',
|
||||
});
|
||||
break;
|
||||
case 'editRule':
|
||||
updatedTitle = i18n.translate('xpack.triggersActionsUI.rules.editRule.breadcrumbTitle', {
|
||||
defaultMessage: 'Edit rule',
|
||||
});
|
||||
break;
|
||||
case 'alerts':
|
||||
updatedTitle = i18n.translate('xpack.triggersActionsUI.alerts.breadcrumbTitle', {
|
||||
defaultMessage: 'Alerts',
|
||||
|
|
|
@ -31,7 +31,7 @@ import type { LensPublicStart } from '@kbn/lens-plugin/public';
|
|||
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public';
|
||||
import { ruleDetailsRoute } from '@kbn/rule-data-utils';
|
||||
import { ruleDetailsRoute, createRuleRoute, editRuleRoute } from '@kbn/rule-data-utils';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { DashboardStart } from '@kbn/dashboard-plugin/public';
|
||||
import { ExpressionsStart } from '@kbn/expressions-plugin/public';
|
||||
|
@ -54,11 +54,14 @@ import { KibanaContextProvider, useKibana } from '../common/lib/kibana';
|
|||
import { ConnectorProvider } from './context/connector_context';
|
||||
import { ALERTS_PAGE_ID, CONNECTORS_PLUGIN_ID } from '../common/constants';
|
||||
import { queryClient } from './query_client';
|
||||
import { getIsExperimentalFeatureEnabled } from '../common/get_experimental_features';
|
||||
|
||||
const TriggersActionsUIHome = lazy(() => import('./home'));
|
||||
const RuleDetailsRoute = lazy(
|
||||
() => import('./sections/rule_details/components/rule_details_route')
|
||||
);
|
||||
const CreateRuleRoute = lazy(() => import('./sections/rule_form/rule_form_route'));
|
||||
const EditRuleRoute = lazy(() => import('./sections/rule_form/rule_form_route'));
|
||||
|
||||
export interface TriggersAndActionsUiServices extends CoreStart {
|
||||
actions: ActionsPublicPluginSetup;
|
||||
|
@ -122,9 +125,25 @@ export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) =
|
|||
application: { navigateToApp },
|
||||
} = useKibana().services;
|
||||
|
||||
const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout');
|
||||
|
||||
return (
|
||||
<ConnectorProvider value={{ services: { validateEmailAddresses } }}>
|
||||
<Routes>
|
||||
{!isUsingRuleCreateFlyout && (
|
||||
<Route
|
||||
exact
|
||||
path={createRuleRoute}
|
||||
component={suspendedComponentWithProps(CreateRuleRoute, 'xl')}
|
||||
/>
|
||||
)}
|
||||
{!isUsingRuleCreateFlyout && (
|
||||
<Route
|
||||
exact
|
||||
path={editRuleRoute}
|
||||
component={suspendedComponentWithProps(EditRuleRoute, 'xl')}
|
||||
/>
|
||||
)}
|
||||
<Route
|
||||
path={`/:section(${sectionsRegex})`}
|
||||
component={suspendedComponentWithProps(TriggersActionsUIHome, 'xl')}
|
||||
|
@ -154,7 +173,6 @@ export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) =
|
|||
return null;
|
||||
}}
|
||||
/>
|
||||
|
||||
<Redirect from={'/'} to="rules" />
|
||||
</Routes>
|
||||
</ConnectorProvider>
|
||||
|
|
|
@ -199,6 +199,7 @@ export function RuleComponent({
|
|||
actionTypeRegistry,
|
||||
ruleTypeRegistry,
|
||||
hideEditButton: true,
|
||||
useNewRuleForm: true,
|
||||
onEditRule: requestRefresh,
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -21,6 +21,10 @@ jest.mock('./rule_actions', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../../common/get_experimental_features', () => ({
|
||||
getIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
jest.mock('../../../lib/capabilities', () => ({
|
||||
hasAllPrivilege: jest.fn(() => true),
|
||||
hasSaveRulesCapability: jest.fn(() => true),
|
||||
|
|
|
@ -16,13 +16,14 @@ import {
|
|||
EuiLoadingSpinner,
|
||||
EuiDescriptionList,
|
||||
} from '@elastic/eui';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { AlertConsumers, getEditRuleRoute, getRuleDetailsRoute } from '@kbn/rule-data-utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { formatDuration } from '@kbn/alerting-plugin/common';
|
||||
import { useLoadRuleTypesQuery } from '../../../hooks/use_load_rule_types_query';
|
||||
import { RuleDefinitionProps } from '../../../../types';
|
||||
import { RuleType } from '../../../..';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
|
||||
import {
|
||||
hasAllPrivilege,
|
||||
hasExecuteActionsCapability,
|
||||
|
@ -38,11 +39,14 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
|
|||
onEditRule,
|
||||
hideEditButton = false,
|
||||
filteredRuleTypes = [],
|
||||
useNewRuleForm = false,
|
||||
}) => {
|
||||
const {
|
||||
application: { capabilities },
|
||||
application: { capabilities, navigateToApp },
|
||||
} = useKibana().services;
|
||||
|
||||
const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout');
|
||||
|
||||
const [editFlyoutVisible, setEditFlyoutVisible] = useState<boolean>(false);
|
||||
const [ruleType, setRuleType] = useState<RuleType>();
|
||||
const {
|
||||
|
@ -103,6 +107,20 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
|
|||
return '';
|
||||
}, [rule, ruleTypeRegistry]);
|
||||
|
||||
const onEditRuleClick = () => {
|
||||
if (!isUsingRuleCreateFlyout && useNewRuleForm) {
|
||||
navigateToApp('management', {
|
||||
path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(rule.id)}`,
|
||||
state: {
|
||||
returnApp: 'management',
|
||||
returnPath: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(rule.id)}`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setEditFlyoutVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const ruleDefinitionList = [
|
||||
{
|
||||
title: i18n.translate('xpack.triggersActionsUI.ruleDetails.ruleType', {
|
||||
|
@ -153,7 +171,7 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
|
|||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{hasEditButton ? (
|
||||
<EuiButtonEmpty onClick={() => setEditFlyoutVisible(true)} flush="left">
|
||||
<EuiButtonEmpty onClick={onEditRuleClick} flush="left">
|
||||
<EuiText size="s">{getRuleConditionsWording()}</EuiText>
|
||||
</EuiButtonEmpty>
|
||||
) : (
|
||||
|
@ -206,7 +224,7 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
|
|||
<EuiButtonEmpty
|
||||
data-test-subj="ruleDetailsEditButton"
|
||||
iconType={'pencil'}
|
||||
onClick={() => setEditFlyoutVisible(true)}
|
||||
onClick={onEditRuleClick}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)
|
||||
|
|
|
@ -23,6 +23,10 @@ import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock';
|
|||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
jest.mock('../../../../common/get_experimental_features', () => ({
|
||||
getIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_ui_config', () => ({
|
||||
fetchUiConfig: jest
|
||||
.fn()
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { RuleExecutionStatusErrorReasons, parseDuration } from '@kbn/alerting-plugin/common';
|
||||
import { getRuleDetailsRoute } from '@kbn/rule-data-utils';
|
||||
import { getEditRuleRoute, getRuleDetailsRoute } from '@kbn/rule-data-utils';
|
||||
import { fetchUiConfig as triggersActionsUiConfig } from '@kbn/alerts-ui-shared/src/common/apis/fetch_ui_config';
|
||||
import { UpdateApiKeyModalConfirmation } from '../../../components/update_api_key_modal_confirmation';
|
||||
import { bulkUpdateAPIKey } from '../../../lib/rule_api/update_api_key';
|
||||
|
@ -71,6 +71,7 @@ import {
|
|||
import { useBulkOperationToast } from '../../../hooks/use_bulk_operation_toast';
|
||||
import { RefreshToken } from './types';
|
||||
import { UntrackAlertsModal } from '../../common/components/untrack_alerts_modal';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
|
||||
|
||||
export type RuleDetailsProps = {
|
||||
rule: Rule;
|
||||
|
@ -78,6 +79,7 @@ export type RuleDetailsProps = {
|
|||
actionTypes: ActionType[];
|
||||
requestRefresh: () => Promise<void>;
|
||||
refreshToken?: RefreshToken;
|
||||
useNewRuleForm?: boolean;
|
||||
} & Pick<
|
||||
BulkOperationsComponentOpts,
|
||||
'bulkDisableRules' | 'bulkEnableRules' | 'bulkDeleteRules' | 'snoozeRule' | 'unsnoozeRule'
|
||||
|
@ -98,7 +100,7 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
}) => {
|
||||
const history = useHistory();
|
||||
const {
|
||||
application: { capabilities },
|
||||
application: { capabilities, navigateToApp },
|
||||
ruleTypeRegistry,
|
||||
actionTypeRegistry,
|
||||
setBreadcrumbs,
|
||||
|
@ -108,6 +110,9 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
theme,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout');
|
||||
|
||||
const ruleReducer = useMemo(() => getRuleReducer(actionTypeRegistry), [actionTypeRegistry]);
|
||||
const [{}, dispatch] = useReducer(ruleReducer, { rule });
|
||||
const setInitialRule = (value: Rule) => {
|
||||
|
@ -206,7 +211,7 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
data-test-subj="ruleIntervalToastEditButton"
|
||||
onClick={() => {
|
||||
toasts.remove(configurationToast);
|
||||
setEditFlyoutVisibility(true);
|
||||
onEditRuleClick();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -223,6 +228,7 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
});
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
i18nStart,
|
||||
theme,
|
||||
|
@ -256,12 +262,26 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const onEditRuleClick = () => {
|
||||
if (!isUsingRuleCreateFlyout) {
|
||||
navigateToApp('management', {
|
||||
path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(rule.id)}`,
|
||||
state: {
|
||||
returnApp: 'management',
|
||||
returnPath: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(rule.id)}`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setEditFlyoutVisibility(true);
|
||||
}
|
||||
};
|
||||
|
||||
const editButton = hasEditButton ? (
|
||||
<>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="openEditRuleFlyoutButton"
|
||||
iconType="pencil"
|
||||
onClick={() => setEditFlyoutVisibility(true)}
|
||||
onClick={onEditRuleClick}
|
||||
name="edit"
|
||||
disabled={!ruleType.enabledInLicense}
|
||||
>
|
||||
|
@ -529,7 +549,7 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
<EuiLink
|
||||
data-test-subj="actionWithBrokenConnectorWarningBannerEdit"
|
||||
color="primary"
|
||||
onClick={() => setEditFlyoutVisibility(true)}
|
||||
onClick={onEditRuleClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.actionWithBrokenConnectorWarningBannerEditText"
|
||||
|
|
|
@ -24,6 +24,11 @@ jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_ui_config', () => ({
|
|||
.fn()
|
||||
.mockResolvedValue({ minimumScheduleInterval: { value: '1m', enforce: false } }),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../common/get_experimental_features', () => ({
|
||||
getIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
describe('rule_details_route', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 React, { useEffect } from 'react';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { RuleForm } from '@kbn/alerts-ui-shared/src/rule_form/rule_form';
|
||||
import { getRuleDetailsRoute } from '@kbn/rule-data-utils';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { getAlertingSectionBreadcrumb } from '../../lib/breadcrumb';
|
||||
import { getCurrentDocTitle } from '../../lib/doc_title';
|
||||
|
||||
export const RuleFormRoute = () => {
|
||||
const {
|
||||
http,
|
||||
i18n,
|
||||
theme,
|
||||
application,
|
||||
notifications,
|
||||
charts,
|
||||
settings,
|
||||
data,
|
||||
dataViews,
|
||||
unifiedSearch,
|
||||
docLinks,
|
||||
ruleTypeRegistry,
|
||||
actionTypeRegistry,
|
||||
chrome,
|
||||
setBreadcrumbs,
|
||||
} = useKibana().services;
|
||||
|
||||
const location = useLocation<{ returnApp?: string; returnPath?: string }>();
|
||||
const { id, ruleTypeId } = useParams<{
|
||||
id?: string;
|
||||
ruleTypeId?: string;
|
||||
}>();
|
||||
const { returnApp, returnPath } = location.state || {};
|
||||
|
||||
// Set breadcrumb and page title
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
setBreadcrumbs([
|
||||
getAlertingSectionBreadcrumb('rules', true),
|
||||
getAlertingSectionBreadcrumb('editRule'),
|
||||
]);
|
||||
chrome.docTitle.change(getCurrentDocTitle('editRule'));
|
||||
}
|
||||
if (ruleTypeId) {
|
||||
setBreadcrumbs([
|
||||
getAlertingSectionBreadcrumb('rules', true),
|
||||
getAlertingSectionBreadcrumb('createRule'),
|
||||
]);
|
||||
chrome.docTitle.change(getCurrentDocTitle('createRule'));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<IntlProvider locale="en">
|
||||
<RuleForm
|
||||
plugins={{
|
||||
http,
|
||||
i18n,
|
||||
theme,
|
||||
application,
|
||||
notifications,
|
||||
charts,
|
||||
settings,
|
||||
data,
|
||||
dataViews,
|
||||
unifiedSearch,
|
||||
docLinks,
|
||||
ruleTypeRegistry,
|
||||
actionTypeRegistry,
|
||||
}}
|
||||
onCancel={() => {
|
||||
if (returnApp && returnPath) {
|
||||
application.navigateToApp(returnApp, { path: returnPath });
|
||||
} else {
|
||||
application.navigateToApp('management', {
|
||||
path: `insightsAndAlerting/triggersActions/rules`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onSubmit={(ruleId) => {
|
||||
application.navigateToApp('management', {
|
||||
path: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(ruleId)}`,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { RuleFormRoute as default };
|
|
@ -45,6 +45,8 @@ import {
|
|||
RuleCreationValidConsumer,
|
||||
ruleDetailsRoute as commonRuleDetailsRoute,
|
||||
STACK_ALERTS_FEATURE_ID,
|
||||
getCreateRuleRoute,
|
||||
getEditRuleRoute,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { MaintenanceWindowCallout } from '@kbn/alerts-ui-shared';
|
||||
import {
|
||||
|
@ -139,6 +141,7 @@ export interface RulesListProps {
|
|||
onRefresh?: (refresh: Date) => void;
|
||||
setHeaderActions?: (components?: React.ReactNode[]) => void;
|
||||
initialSelectedConsumer?: RuleCreationValidConsumer | null;
|
||||
useNewRuleForm?: boolean;
|
||||
}
|
||||
|
||||
export const percentileFields = {
|
||||
|
@ -180,12 +183,13 @@ export const RulesList = ({
|
|||
onRefresh,
|
||||
setHeaderActions,
|
||||
initialSelectedConsumer = STACK_ALERTS_FEATURE_ID,
|
||||
useNewRuleForm = false,
|
||||
}: RulesListProps) => {
|
||||
const history = useHistory();
|
||||
const kibanaServices = useKibana().services;
|
||||
const {
|
||||
actionTypeRegistry,
|
||||
application: { capabilities },
|
||||
application: { capabilities, navigateToApp },
|
||||
http,
|
||||
kibanaFeatures,
|
||||
notifications: { toasts },
|
||||
|
@ -211,6 +215,7 @@ export const RulesList = ({
|
|||
const cloneRuleId = useRef<null | string>(null);
|
||||
|
||||
const isRuleStatusFilterEnabled = getIsExperimentalFeatureEnabled('ruleStatusFilter');
|
||||
const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout');
|
||||
|
||||
const [percentileOptions, setPercentileOptions] =
|
||||
useState<EuiSelectableOption[]>(initialPercentileOptions);
|
||||
|
@ -312,8 +317,18 @@ export const RulesList = ({
|
|||
});
|
||||
|
||||
const onRuleEdit = (ruleItem: RuleTableItem) => {
|
||||
setEditFlyoutVisibility(true);
|
||||
setCurrentRuleToEdit(ruleItem);
|
||||
if (!isUsingRuleCreateFlyout && useNewRuleForm) {
|
||||
navigateToApp('management', {
|
||||
path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(ruleItem.id)}`,
|
||||
state: {
|
||||
returnApp: 'management',
|
||||
returnPath: `insightsAndAlerting/triggersActions/rules`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setEditFlyoutVisibility(true);
|
||||
setCurrentRuleToEdit(ruleItem);
|
||||
}
|
||||
};
|
||||
|
||||
const onRunRule = async (id: string) => {
|
||||
|
@ -1006,9 +1021,15 @@ export const RulesList = ({
|
|||
<RuleTypeModal
|
||||
onClose={() => setRuleTypeModalVisibility(false)}
|
||||
onSelectRuleType={(ruleTypeId) => {
|
||||
setRuleTypeIdToCreate(ruleTypeId);
|
||||
setRuleTypeModalVisibility(false);
|
||||
setRuleFlyoutVisibility(true);
|
||||
if (!isUsingRuleCreateFlyout) {
|
||||
navigateToApp('management', {
|
||||
path: `insightsAndAlerting/triggersActions/${getCreateRuleRoute(ruleTypeId)}`,
|
||||
});
|
||||
} else {
|
||||
setRuleTypeIdToCreate(ruleTypeId);
|
||||
setRuleTypeModalVisibility(false);
|
||||
setRuleFlyoutVisibility(true);
|
||||
}
|
||||
}}
|
||||
http={http}
|
||||
toasts={toasts}
|
||||
|
|
|
@ -24,7 +24,7 @@ describe('getIsExperimentalFeatureEnabled', () => {
|
|||
ruleKqlBar: true,
|
||||
isMustacheAutocompleteOn: false,
|
||||
showMustacheAutocompleteSwitch: false,
|
||||
ruleFormV2: false,
|
||||
isUsingRuleCreateFlyout: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -64,7 +64,7 @@ describe('getIsExperimentalFeatureEnabled', () => {
|
|||
|
||||
expect(result).toEqual(false);
|
||||
|
||||
result = getIsExperimentalFeatureEnabled('ruleFormV2');
|
||||
result = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout');
|
||||
|
||||
expect(result).toEqual(false);
|
||||
|
||||
|
|
|
@ -395,6 +395,7 @@ export interface RuleDefinitionProps<Params extends RuleTypeParams = RuleTypePar
|
|||
onEditRule: () => Promise<void>;
|
||||
hideEditButton?: boolean;
|
||||
filteredRuleTypes?: string[];
|
||||
useNewRuleForm?: boolean;
|
||||
}
|
||||
|
||||
export enum Percentiles {
|
||||
|
|
|
@ -85,6 +85,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
'stackAlertsPage',
|
||||
'ruleTagFilter',
|
||||
'ruleStatusFilter',
|
||||
'isUsingRuleCreateFlyout',
|
||||
])}`,
|
||||
`--xpack.alerting.rules.minimumScheduleInterval.value="2s"`,
|
||||
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
|
||||
|
|
|
@ -32,6 +32,11 @@ export function createTestConfig(options: CreateTestConfigOptions) {
|
|||
serverArgs: [
|
||||
...svlSharedConfig.get('kbnTestServer.serverArgs'),
|
||||
`--serverless=${options.serverlessProject}`,
|
||||
// Ensures the existing E2E tests are backwards compatible with the old rule create flyout
|
||||
// Remove this experiment once all of the migration has been completed
|
||||
`--xpack.trigger_actions_ui.enableExperimental=${JSON.stringify([
|
||||
'isUsingRuleCreateFlyout',
|
||||
])}`,
|
||||
...(options.kbnServerArgs ?? []),
|
||||
],
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue