[RAM] Hide Logs tab in Rules page to unauthorized users, deduplicate rule types requests (#171417)

Closes #158256, #155394

## Summary

- Hides the Logs tab in the Stack Management > Rules page when the user
lacks the necessary permissions to avoid error messages (as shown in
https://github.com/elastic/kibana/issues/158256)
- Switches old `useLoadRuleTypes` hook usages to the new
`useLoadRuleTypesQuery` hook
- Assigns a staleTime to the rule types query to avoid duplicated
requests (as shown in https://github.com/elastic/kibana/issues/155394)

---------

Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
Umberto Pepato 2023-12-05 13:30:58 +01:00 committed by GitHub
parent 727fb131b5
commit 0f5b544b9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 543 additions and 384 deletions

View file

@ -27,6 +27,7 @@ import ActionForm from '@kbn/triggers-actions-ui-plugin/public/application/secti
import { Legacy } from '../legacy_shims';
import { I18nProvider } from '@kbn/i18n-react';
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
interface AlertAction {
group: string;
@ -43,9 +44,12 @@ const { loadActionTypes } = jest.requireMock(
'@kbn/triggers-actions-ui-plugin/public/application/lib/action_connector_api'
);
jest.mock('@kbn/triggers-actions-ui-plugin/public/application/lib/rule_api', () => ({
loadAlertTypes: jest.fn(),
}));
jest.mock(
'@kbn/triggers-actions-ui-plugin/public/application/hooks/use_load_rule_types_query',
() => ({
useLoadRuleTypesQuery: jest.fn(),
})
);
jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({
useUiSetting: jest.fn().mockImplementation((_, defaultValue) => defaultValue),
@ -109,6 +113,14 @@ describe('alert_form', () => {
let wrapper: ReactWrapper<any>;
beforeEach(async () => {
const { useLoadRuleTypesQuery } = jest.requireMock(
'@kbn/triggers-actions-ui-plugin/public/application/hooks/use_load_rule_types_query'
);
useLoadRuleTypesQuery.mockReturnValue({
ruleTypesState: {
data: new Map(),
},
});
ruleTypeRegistry.list.mockReturnValue([ruleType]);
ruleTypeRegistry.get.mockReturnValue(ruleType);
ruleTypeRegistry.has.mockReturnValue(true);
@ -136,19 +148,31 @@ describe('alert_form', () => {
wrapper = mountWithIntl(
<I18nProvider>
<KibanaReactContext.Provider>
<RuleForm
rule={initialAlert}
config={{
isUsingSecurity: true,
minimumScheduleInterval: { value: '1m', enforce: false },
}}
dispatch={() => {}}
errors={{ name: [], 'schedule.interval': [] }}
operation="create"
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}
onChangeMetaData={() => {}}
/>
<QueryClientProvider
client={
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
}
>
<RuleForm
rule={initialAlert}
config={{
isUsingSecurity: true,
minimumScheduleInterval: { value: '1m', enforce: false },
}}
dispatch={() => {}}
errors={{ name: [], 'schedule.interval': [] }}
operation="create"
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}
onChangeMetaData={() => {}}
/>
</QueryClientProvider>
</KibanaReactContext.Provider>
</I18nProvider>
);

View file

@ -28,7 +28,7 @@ jest.mock('../../utils/kibana_react', () => ({
jest.mock('@kbn/observability-shared-plugin/public');
jest.mock('@kbn/triggers-actions-ui-plugin/public', () => ({
useLoadRuleTypes: jest.fn(),
useLoadRuleTypesQuery: jest.fn(),
}));
jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({
@ -55,7 +55,7 @@ jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({
plugins: {} as ObservabilityPublicPluginsStart,
}));
const { useLoadRuleTypes } = jest.requireMock('@kbn/triggers-actions-ui-plugin/public');
const { useLoadRuleTypesQuery } = jest.requireMock('@kbn/triggers-actions-ui-plugin/public');
describe('RulesPage with all capabilities', () => {
async function setup() {
@ -65,41 +65,54 @@ describe('RulesPage with all capabilities', () => {
enabledInLicense: true,
id: '1',
name: 'test rule',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
minimumLicenseRequired: 'basic',
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { all: true },
},
ruleTaskTimeout: '1m',
},
'2': {
enabledInLicense: true,
id: '2',
name: 'test rule ok',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
minimumLicenseRequired: 'basic',
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { all: true },
},
ruleTaskTimeout: '1m',
},
'3': {
enabledInLicense: true,
id: '3',
name: 'test rule pending',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
minimumLicenseRequired: 'basic',
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { all: true },
},
ruleTaskTimeout: '1m',
},
})
);
const ruleTypes = [
{
id: 'test_rule_type',
name: 'some rule type',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { all: true },
},
ruleTaskTimeout: '1m',
useLoadRuleTypesQuery.mockReturnValue({
ruleTypesState: {
data: ruleTypeIndex,
},
];
useLoadRuleTypes.mockReturnValue({
ruleTypes,
ruleTypeIndex,
});
return render(
@ -133,9 +146,29 @@ describe('RulesPage with show only capability', () => {
enabledInLicense: true,
id: '1',
name: 'test rule',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
minimumLicenseRequired: 'basic',
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: false },
},
ruleTaskTimeout: '1m',
},
'2': {
enabledInLicense: true,
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
minimumLicenseRequired: 'basic',
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: false },
},
ruleTaskTimeout: '1m',
id: '2',
name: 'test rule ok',
},
@ -143,28 +176,25 @@ describe('RulesPage with show only capability', () => {
enabledInLicense: true,
id: '3',
name: 'test rule pending',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
minimumLicenseRequired: 'basic',
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: false },
},
ruleTaskTimeout: '1m',
},
})
);
const ruleTypes = [
{
id: 'test_rule_type',
name: 'some rule type',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: false },
},
ruleTaskTimeout: '1m',
useLoadRuleTypesQuery.mockReturnValue({
ruleTypesState: {
data: ruleTypeIndex,
},
];
useLoadRuleTypes.mockReturnValue({ ruleTypes, ruleTypeIndex });
});
return render(<RulesPage />);
}

View file

@ -10,11 +10,11 @@ import { useHistory } from 'react-router-dom';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useLoadRuleTypes } from '@kbn/triggers-actions-ui-plugin/public';
import { ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common';
import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { useLoadRuleTypesQuery } from '@kbn/triggers-actions-ui-plugin/public';
import { RULES_LOGS_PATH, RULES_PATH } from '../../../common/locators/paths';
import { useKibana } from '../../utils/kibana_react';
import { usePluginContext } from '../../hooks/use_plugin_context';
@ -57,7 +57,9 @@ export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) {
]);
const filteredRuleTypes = useGetFilteredRuleTypes();
const { ruleTypes } = useLoadRuleTypes({
const {
ruleTypesState: { data: ruleTypes },
} = useLoadRuleTypesQuery({
filteredRuleTypes,
});

View file

@ -28,7 +28,7 @@ import { Storage } from '@kbn/kibana-utils-plugin/public';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public';
import { ruleDetailsRoute } from '@kbn/rule-data-utils';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { DashboardStart } from '@kbn/dashboard-plugin/public';
import { ExpressionsStart } from '@kbn/expressions-plugin/public';
import { suspendedComponentWithProps } from './lib/suspended_component_with_props';
@ -43,12 +43,12 @@ import { setDataViewsService } from '../common/lib/data_apis';
import { KibanaContextProvider, useKibana } from '../common/lib/kibana';
import { ConnectorProvider } from './context/connector_context';
import { CONNECTORS_PLUGIN_ID } from '../common/constants';
import { queryClient } from './query_client';
const TriggersActionsUIHome = lazy(() => import('./home'));
const RuleDetailsRoute = lazy(
() => import('./sections/rule_details/components/rule_details_route')
);
const queryClient = new QueryClient();
export interface TriggersAndActionsUiServices extends CoreStart {
actions: ActionsPublicPluginSetup;

View file

@ -16,6 +16,7 @@ import { mountWithIntl } from '@kbn/test-jest-helpers';
import TriggersActionsUIHome, { MatchParams } from './home';
import { hasShowActionsCapability } from './lib/capabilities';
import { getIsExperimentalFeatureEnabled } from '../common/get_experimental_features';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
jest.mock('../common/lib/kibana');
jest.mock('../common/get_experimental_features');
@ -32,10 +33,21 @@ jest.mock('./context/health_context', () => ({
HealthContextProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
jest.mock('./hooks/use_load_rule_types_query', () => ({
useLoadRuleTypesQuery: jest.fn().mockReturnValue({
authorizedToReadAnyRules: true,
}),
}));
const { useLoadRuleTypesQuery } = jest.requireMock('./hooks/use_load_rule_types_query');
const queryClient = new QueryClient();
describe('home', () => {
beforeEach(() => {
(hasShowActionsCapability as jest.Mock).mockClear();
(getIsExperimentalFeatureEnabled as jest.Mock).mockClear();
(getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => false);
useLoadRuleTypesQuery.mockClear();
});
it('renders rule list components', async () => {
@ -57,7 +69,9 @@ describe('home', () => {
render(
<IntlProvider locale="en">
<Router history={props.history}>
<TriggersActionsUIHome {...props} />
<QueryClientProvider client={queryClient}>
<TriggersActionsUIHome {...props} />
</QueryClientProvider>
</Router>
</IntlProvider>
);
@ -67,7 +81,7 @@ describe('home', () => {
});
});
it('hides the internal alerts table route if the config is not set', async () => {
it('hides the internal alerts table tab if the config is not set', async () => {
(hasShowActionsCapability as jest.Mock).mockImplementation(() => {
return true;
});
@ -76,7 +90,7 @@ describe('home', () => {
location: createLocation('/'),
match: {
isExact: true,
path: `/connectorss`,
path: `/connectors`,
url: '',
params: {
section: 'connectors',
@ -86,26 +100,58 @@ describe('home', () => {
let home = mountWithIntl(
<Router history={props.history}>
<TriggersActionsUIHome {...props} />
<QueryClientProvider client={queryClient}>
<TriggersActionsUIHome {...props} />
</QueryClientProvider>
</Router>
);
// Just rules/logs
// Just rules and logs
expect(home.find('span.euiTab__content').length).toBe(2);
(getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'internalAlertsTable') {
return true;
}
return false;
return feature === 'internalAlertsTable';
});
home = mountWithIntl(
<Router history={props.history}>
<TriggersActionsUIHome {...props} />
<QueryClientProvider client={queryClient}>
<TriggersActionsUIHome {...props} />
</QueryClientProvider>
</Router>
);
// alerts now too!
expect(home.find('span.euiTab__content').length).toBe(3);
});
it('hides the logs tab if the read rules privilege is missing', async () => {
useLoadRuleTypesQuery.mockReturnValue({
authorizedToReadAnyRules: false,
});
const props: RouteComponentProps<MatchParams> = {
history: createMemoryHistory({
initialEntries: ['/rules'],
}),
location: createLocation('/rules'),
match: {
isExact: true,
path: `/rules`,
url: '',
params: {
section: 'rules',
},
},
};
const home = mountWithIntl(
<Router history={props.history}>
<QueryClientProvider client={queryClient}>
<TriggersActionsUIHome {...props} />
</QueryClientProvider>
</Router>
);
// Just rules
expect(home.find('span.euiTab__content').length).toBe(1);
});
});

View file

@ -21,6 +21,7 @@ import { HealthCheck } from './components/health_check';
import { HealthContextProvider } from './context/health_context';
import { useKibana } from '../common/lib/kibana';
import { suspendedComponentWithProps } from './lib/suspended_component_with_props';
import { useLoadRuleTypesQuery } from './hooks/use_load_rule_types_query';
const RulesList = lazy(() => import('./sections/rules_list/components/rules_list'));
const LogsList = lazy(
@ -41,6 +42,7 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
const [headerActions, setHeaderActions] = useState<React.ReactNode[] | undefined>();
const { chrome, setBreadcrumbs } = useKibana().services;
const isInternalAlertsTableEnabled = getIsExperimentalFeatureEnabled('internalAlertsTable');
const { authorizedToReadAnyRules } = useLoadRuleTypesQuery({ filteredRuleTypes: [] });
const tabs: Array<{
id: Section;
@ -54,10 +56,14 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
),
});
tabs.push({
id: 'logs',
name: <FormattedMessage id="xpack.triggersActionsUI.home.logsTabTitle" defaultMessage="Logs" />,
});
if (authorizedToReadAnyRules) {
tabs.push({
id: 'logs',
name: (
<FormattedMessage id="xpack.triggersActionsUI.home.logsTabTitle" defaultMessage="Logs" />
),
});
}
if (isInternalAlertsTableEnabled) {
tabs.push({

View file

@ -6,4 +6,4 @@
*/
export { useSubAction } from './use_sub_action';
export { useLoadRuleTypes } from './use_load_rule_types';
export { useLoadRuleTypesQuery } from './use_load_rule_types_query';

View file

@ -1,71 +0,0 @@
/*
* 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 { useEffect, useState, useRef } from 'react';
import { loadRuleTypes } from '../lib/rule_api/rule_types';
import { RuleType, RuleTypeIndex } from '../../types';
import { useKibana } from '../../common/lib/kibana';
interface RuleTypesState {
isLoading: boolean;
data: Array<RuleType<string, string>>;
error: string | null;
}
interface RuleTypesProps {
filteredRuleTypes?: string[];
}
export function useLoadRuleTypes({ filteredRuleTypes }: RuleTypesProps) {
const { http } = useKibana().services;
const isMounted = useRef(false);
const [ruleTypesState, setRuleTypesState] = useState<RuleTypesState>({
isLoading: true,
data: [],
error: null,
});
const [ruleTypeIndex, setRuleTypeIndex] = useState<RuleTypeIndex>(new Map());
async function fetchRuleTypes() {
try {
const response = await loadRuleTypes({ http });
const index: RuleTypeIndex = new Map();
for (const ruleTypeItem of response) {
index.set(ruleTypeItem.id, ruleTypeItem);
}
if (isMounted.current) {
setRuleTypeIndex(index);
let filteredResponse = response;
if (filteredRuleTypes && filteredRuleTypes.length > 0) {
filteredResponse = response.filter((item) => filteredRuleTypes.includes(item.id));
}
setRuleTypesState({ ...ruleTypesState, isLoading: false, data: filteredResponse });
}
} catch (e) {
if (isMounted.current) {
setRuleTypesState({ ...ruleTypesState, isLoading: false, error: e });
}
}
}
useEffect(() => {
isMounted.current = true;
fetchRuleTypes();
return () => {
isMounted.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return {
ruleTypes: ruleTypesState.data,
error: ruleTypesState.error,
ruleTypeIndex,
ruleTypesIsLoading: ruleTypesState.isLoading,
};
}

View file

@ -7,6 +7,7 @@
import { i18n } from '@kbn/i18n';
import { useQuery } from '@tanstack/react-query';
import { ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common';
import { useMemo } from 'react';
import { loadRuleTypes } from '../lib/rule_api/rule_types';
import { useKibana } from '../../common/lib/kibana';
import { RuleType, RuleTypeIndex } from '../../types';
@ -32,8 +33,10 @@ const getFilteredIndex = (data: Array<RuleType<string, string>>, filteredRuleTyp
return filteredIndex;
};
export const useLoadRuleTypesQuery = (props: UseLoadRuleTypesQueryProps) => {
const { filteredRuleTypes, enabled = true } = props;
export const useLoadRuleTypesQuery = ({
filteredRuleTypes,
enabled = true,
}: UseLoadRuleTypesQueryProps) => {
const {
http,
notifications: { toasts },
@ -51,18 +54,24 @@ export const useLoadRuleTypesQuery = (props: UseLoadRuleTypesQueryProps) => {
);
};
const { data, isSuccess, isFetching, isInitialLoading, isLoading } = useQuery({
const { data, isSuccess, isFetching, isInitialLoading, isLoading, error } = useQuery({
queryKey: ['loadRuleTypes'],
queryFn,
onError: onErrorFn,
refetchOnWindowFocus: false,
// Leveraging TanStack Query's caching system to avoid duplicated requests as
// other state-sharing solutions turned out to be overly complex and less readable
staleTime: 60 * 1000,
enabled,
});
const filteredIndex = data ? getFilteredIndex(data, filteredRuleTypes) : new Map();
const filteredIndex = useMemo(
() => (data ? getFilteredIndex(data, filteredRuleTypes) : new Map<string, RuleType>()),
[data, filteredRuleTypes]
);
const hasAnyAuthorizedRuleType = filteredIndex.size > 0;
const authorizedRuleTypes = [...filteredIndex.values()];
const authorizedRuleTypes = useMemo(() => [...filteredIndex.values()], [filteredIndex]);
const authorizedToCreateAnyRules = authorizedRuleTypes.some(
(ruleType) => ruleType.authorizedConsumers[ALERTS_FEATURE_ID]?.all
);
@ -75,6 +84,7 @@ export const useLoadRuleTypesQuery = (props: UseLoadRuleTypesQueryProps) => {
initialLoad: isLoading || isInitialLoading,
isLoading: isLoading || isFetching,
data: filteredIndex,
error,
},
hasAnyAuthorizedRuleType,
authorizedRuleTypes,

View file

@ -0,0 +1,13 @@
/*
* 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 { QueryClient } from '@tanstack/react-query';
/**
* The main query client for the triggers_actions_ui app
*/
export const queryClient = new QueryClient();

View file

@ -13,6 +13,7 @@ import { RuleDefinition } from './rule_definition';
import { actionTypeRegistryMock } from '../../../action_type_registry.mock';
import { ActionTypeModel, Rule, RuleTypeModel } from '../../../../types';
import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
jest.mock('./rule_actions', () => ({
RuleActions: () => {
@ -28,27 +29,10 @@ jest.mock('../../../lib/capabilities', () => ({
hasManageApiKeysCapability: jest.fn(() => true),
}));
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../..', () => ({
useLoadRuleTypes: jest.fn(),
jest.mock('../../../hooks/use_load_rule_types_query', () => ({
useLoadRuleTypesQuery: jest.fn(),
}));
const { useLoadRuleTypes } = jest.requireMock('../../../..');
const ruleTypes = [
{
id: 'test_rule_type',
name: 'some rule type',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: false },
},
ruleTaskTimeout: '1m',
},
];
const { useLoadRuleTypesQuery } = jest.requireMock('../../../hooks/use_load_rule_types_query');
const mockedRuleTypeIndex = new Map(
Object.entries({
@ -56,16 +40,46 @@ const mockedRuleTypeIndex = new Map(
enabledInLicense: true,
id: 'test_rule_type',
name: 'test rule',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
minimumLicenseRequired: 'basic',
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: false },
},
ruleTaskTimeout: '1m',
},
'2': {
enabledInLicense: true,
id: '2',
name: 'test rule ok',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
minimumLicenseRequired: 'basic',
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: false },
},
ruleTaskTimeout: '1m',
},
'3': {
enabledInLicense: true,
id: '3',
name: 'test rule pending',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
minimumLicenseRequired: 'basic',
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: false },
},
ruleTaskTimeout: '1m',
},
})
);
@ -116,15 +130,17 @@ describe('Rule Definition', () => {
{ id: '.index', iconClass: 'indexOpen' },
] as ActionTypeModel[]);
useLoadRuleTypes.mockReturnValue({ ruleTypes, ruleTypeIndex: mockedRuleTypeIndex });
useLoadRuleTypesQuery.mockReturnValue({ ruleTypesState: { data: mockedRuleTypeIndex } });
wrapper = mount(
<RuleDefinition
rule={mockedRule}
actionTypeRegistry={actionTypeRegistry}
onEditRule={jest.fn()}
ruleTypeRegistry={ruleTypeRegistry}
/>
<QueryClientProvider client={new QueryClient()}>
<RuleDefinition
rule={mockedRule}
actionTypeRegistry={actionTypeRegistry}
onEditRule={jest.fn()}
ruleTypeRegistry={ruleTypeRegistry}
/>
</QueryClientProvider>
);
await act(async () => {
await nextTick();
@ -141,8 +157,8 @@ describe('Rule Definition', () => {
expect(wrapper.find('[data-test-subj="ruleSummaryRuleDefinition"]')).toBeTruthy();
});
it('show rule type name from "useLoadRuleTypes"', async () => {
expect(useLoadRuleTypes).toHaveBeenCalledTimes(2);
it('show rule type name from "useLoadRuleTypesQuery"', async () => {
expect(useLoadRuleTypesQuery).toHaveBeenCalledTimes(2);
const ruleType = wrapper.find('[data-test-subj="ruleSummaryRuleType"]');
expect(ruleType).toBeTruthy();
expect(ruleType.find('div.euiText').text()).toEqual(

View file

@ -18,8 +18,9 @@ import {
import { AlertConsumers } 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, useLoadRuleTypes } from '../../../..';
import { RuleType } from '../../../..';
import { useKibana } from '../../../../common/lib/kibana';
import {
hasAllPrivilege,
@ -35,7 +36,7 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
ruleTypeRegistry,
onEditRule,
hideEditButton = false,
filteredRuleTypes,
filteredRuleTypes = [],
}) => {
const {
application: { capabilities },
@ -43,9 +44,12 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
const [editFlyoutVisible, setEditFlyoutVisible] = useState<boolean>(false);
const [ruleType, setRuleType] = useState<RuleType>();
const { ruleTypes, ruleTypeIndex, ruleTypesIsLoading } = useLoadRuleTypes({
const {
ruleTypesState: { data: ruleTypeIndex, isLoading: ruleTypesIsLoading },
} = useLoadRuleTypesQuery({
filteredRuleTypes,
});
const ruleTypes = useMemo(() => [...ruleTypeIndex.values()], [ruleTypeIndex]);
const getRuleType = useMemo(() => {
if (ruleTypes.length && rule) {

View file

@ -34,7 +34,8 @@ import { useKibana } from '../../../common/lib/kibana';
import { triggersActionsUiConfig } from '../../../common/lib/config_api';
import { triggersActionsUiHealth } from '../../../common/lib/health_api';
import { loadActionTypes, loadAllActions } from '../../lib/action_connector_api';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { waitFor } from '@testing-library/react';
jest.mock('../../../common/lib/kibana');
jest.mock('../../lib/rule_api/rule_types', () => ({
@ -192,19 +193,21 @@ describe('rule_add', () => {
actionTypeRegistry.has.mockReturnValue(true);
wrapper = mountWithIntl(
<RuleAdd
consumer={ALERTS_FEATURE_ID}
onClose={onClose}
initialValues={initialValues}
onSave={() => {
return new Promise<void>(() => {});
}}
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}
metadata={{ test: 'some value', fields: ['test'] }}
ruleTypeId={ruleTypeId}
validConsumers={validConsumers}
/>
<QueryClientProvider client={new QueryClient()}>
<RuleAdd
consumer={ALERTS_FEATURE_ID}
onClose={onClose}
initialValues={initialValues}
onSave={() => {
return new Promise<void>(() => {});
}}
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}
metadata={{ test: 'some value', fields: ['test'] }}
ruleTypeId={ruleTypeId}
validConsumers={validConsumers}
/>
</QueryClientProvider>
);
// Wait for active space to resolve before requesting the component to update
@ -239,7 +242,7 @@ describe('rule_add', () => {
});
});
it('renders a confirm close modal if the flyout is closed after inputs have changed', async () => {
it('renders selection of rule types to pick in the modal', async () => {
(triggersActionsUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '1m', enforce: false },
});
@ -254,14 +257,47 @@ describe('rule_add', () => {
wrapper.update();
});
wrapper.find('[data-test-subj="my-rule-type-SelectOption"]').last().simulate('click');
expect(wrapper.find('input#ruleName').props().value).toBe('');
expect(wrapper.find('[data-test-subj="tagsComboBox"]').first().text()).toBe('');
expect(wrapper.find('.euiSelect').first().props().value).toBe('m');
await waitFor(() => {
const ruleTypesContainer = wrapper.find('[data-test-subj="ruleGroupTypeSelectContainer"]');
const ruleTypeButton = ruleTypesContainer
.render()
.find('[data-test-subj="my-rule-type-SelectOption"]');
wrapper.find('[data-test-subj="cancelSaveRuleButton"]').last().simulate('click');
expect(onClose).not.toHaveBeenCalled();
expect(wrapper.find('[data-test-subj="confirmRuleCloseModal"]').exists()).toBe(true);
expect(ruleTypeButton.length).toEqual(1);
expect(ruleTypeButton.text()).toMatchInlineSnapshot(`"Testtest"`);
});
});
it('renders a confirm close modal if the flyout is closed after inputs have changed', async () => {
(triggersActionsUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '1m', enforce: false },
});
const onClose = jest.fn();
await setup({
initialValues: {},
onClose,
ruleTypeId: 'my-rule-type',
});
await act(async () => {
await nextTick();
wrapper.update();
});
wrapper
.find('input#ruleName')
.at(0)
.simulate('change', { target: { value: 'my rule type' } });
await waitFor(() => {
expect(wrapper.find('input#ruleName').props().value).toBe('my rule type');
expect(wrapper.find('[data-test-subj="tagsComboBox"]').first().text()).toBe('');
expect(wrapper.find('.euiSelect').first().props().value).toBe('m');
wrapper.find('[data-test-subj="cancelSaveRuleButton"]').last().simulate('click');
expect(onClose).not.toHaveBeenCalled();
expect(wrapper.find('[data-test-subj="confirmRuleCloseModal"]').exists()).toBe(true);
});
});
it('renders rule add flyout with initial values', async () => {
@ -410,13 +446,15 @@ describe('rule_add', () => {
wrapper.update();
});
expect(createRule).toHaveBeenLastCalledWith(
expect.objectContaining({
rule: expect.objectContaining({
consumer: 'logs',
}),
})
);
await waitFor(() => {
expect(createRule).toHaveBeenLastCalledWith(
expect.objectContaining({
rule: expect.objectContaining({
consumer: 'logs',
}),
})
);
});
});
it('should enforce any default interval', async () => {
@ -437,14 +475,16 @@ describe('rule_add', () => {
wrapper.update();
});
const intervalInputUnit = wrapper
.find('[data-test-subj="intervalInputUnit"]')
.first()
.getElement().props.value;
const intervalInput = wrapper.find('[data-test-subj="intervalInput"]').first().getElement()
.props.value;
expect(intervalInputUnit).toBe('h');
expect(intervalInput).toBe(3);
await waitFor(() => {
const intervalInputUnit = wrapper
.find('[data-test-subj="intervalInputUnit"]')
.first()
.getElement().props.value;
const intervalInput = wrapper.find('[data-test-subj="intervalInput"]').first().getElement()
.props.value;
expect(intervalInputUnit).toBe('h');
expect(intervalInput).toBe(3);
});
});
it('should load connectors and connector types when there is a pre-selected rule type', async () => {
@ -459,10 +499,18 @@ describe('rule_add', () => {
actionsShow: true,
});
expect(triggersActionsUiHealth).toHaveBeenCalledTimes(1);
expect(alertingFrameworkHealth).toHaveBeenCalledTimes(1);
expect(loadActionTypes).toHaveBeenCalledTimes(1);
expect(loadAllActions).toHaveBeenCalledTimes(1);
// Wait for handlers to fire
await act(async () => {
await nextTick();
wrapper.update();
});
await waitFor(() => {
expect(triggersActionsUiHealth).toHaveBeenCalledTimes(1);
expect(alertingFrameworkHealth).toHaveBeenCalledTimes(1);
expect(loadActionTypes).toHaveBeenCalledTimes(1);
expect(loadAllActions).toHaveBeenCalledTimes(1);
});
});
it('should not load connectors and connector types when there is not an encryptionKey', async () => {
@ -481,13 +529,21 @@ describe('rule_add', () => {
actionsShow: true,
});
expect(triggersActionsUiHealth).toHaveBeenCalledTimes(1);
expect(alertingFrameworkHealth).toHaveBeenCalledTimes(1);
expect(loadActionTypes).not.toHaveBeenCalled();
expect(loadAllActions).not.toHaveBeenCalled();
expect(wrapper.find('[data-test-subj="actionNeededEmptyPrompt"]').first().text()).toContain(
'You must configure an encryption key to use Alerting'
);
// Wait for handlers to fire
await act(async () => {
await nextTick();
wrapper.update();
});
await waitFor(() => {
expect(triggersActionsUiHealth).toHaveBeenCalledTimes(1);
expect(alertingFrameworkHealth).toHaveBeenCalledTimes(1);
expect(loadActionTypes).not.toHaveBeenCalled();
expect(loadAllActions).not.toHaveBeenCalled();
expect(wrapper.find('[data-test-subj="actionNeededEmptyPrompt"]').first().text()).toContain(
'You must configure an encryption key to use Alerting'
);
});
});
});

View file

@ -16,6 +16,7 @@ import { ReactWrapper } from 'enzyme';
import RuleEdit from './rule_edit';
import { useKibana } from '../../../common/lib/kibana';
import { ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
jest.mock('../../../common/lib/kibana');
const actionTypeRegistry = actionTypeRegistryMock.create();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
@ -186,15 +187,17 @@ describe('rule_edit', () => {
actionTypeRegistry.has.mockReturnValue(true);
wrapper = mountWithIntl(
<RuleEdit
onClose={() => {}}
initialRule={rule}
onSave={() => {
return new Promise<void>(() => {});
}}
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}
/>
<QueryClientProvider client={new QueryClient()}>
<RuleEdit
onClose={() => {}}
initialRule={rule}
onSave={() => {
return new Promise<void>(() => {});
}}
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}
/>
</QueryClientProvider>
);
// Wait for active space to resolve before requesting the component to update
await act(async () => {

View file

@ -29,6 +29,11 @@ import { coreMock } from '@kbn/core/public/mocks';
import { ALERTS_FEATURE_ID, RecoveredActionGroup } from '@kbn/alerting-plugin/common';
import { useKibana } from '../../../common/lib/kibana';
const toMapById = [
(acc: Map<unknown, unknown>, val: { id: unknown }) => acc.set(val.id, val),
new Map(),
] as const;
const actionTypeRegistry = actionTypeRegistryMock.create();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
@ -46,8 +51,8 @@ export const TestExpression: FunctionComponent<any> = () => {
);
};
jest.mock('../../hooks/use_load_rule_types', () => ({
useLoadRuleTypes: jest.fn(),
jest.mock('../../hooks/use_load_rule_types_query', () => ({
useLoadRuleTypesQuery: jest.fn(),
}));
jest.mock('../../../common/lib/kibana');
jest.mock('../../lib/capabilities', () => ({
@ -113,7 +118,7 @@ describe('rule_form', () => {
async function setup(enforceMinimum = false, schedule = '1m') {
const mocks = coreMock.createSetup();
const { useLoadRuleTypes } = jest.requireMock('../../hooks/use_load_rule_types');
const { useLoadRuleTypesQuery } = jest.requireMock('../../hooks/use_load_rule_types_query');
const myRuleModel = {
id: 'my-rule-type',
description: 'Sample rule type model',
@ -172,12 +177,13 @@ describe('rule_form', () => {
},
enabledInLicense: false,
};
useLoadRuleTypes.mockReturnValue({
ruleTypes: [myRule, disabledByLicenseRule],
ruleTypeIndex: new Map([
[myRule.id, myRule],
[disabledByLicenseRule.id, disabledByLicenseRule],
]),
useLoadRuleTypesQuery.mockReturnValue({
ruleTypesState: {
data: new Map([
[myRule.id, myRule],
[disabledByLicenseRule.id, disabledByLicenseRule],
]),
},
});
const [
{
@ -278,7 +284,7 @@ describe('rule_form', () => {
} = options || {};
const mocks = coreMock.createSetup();
const { useLoadRuleTypes } = jest.requireMock('../../hooks/use_load_rule_types');
const { useLoadRuleTypesQuery } = jest.requireMock('../../hooks/use_load_rule_types_query');
const ruleTypes: RuleType[] = ruleTypesOverwrite || [
{
id: 'my-rule-type',
@ -327,11 +333,11 @@ describe('rule_form', () => {
enabledInLicense: false,
},
];
const ruleTypeIndex = ruleTypes.reduce((acc, item) => {
acc.set(item.id, item);
return acc;
}, new Map());
useLoadRuleTypes.mockReturnValue({ ruleTypes, ruleTypeIndex });
useLoadRuleTypesQuery.mockReturnValue({
ruleTypesState: {
data: ruleTypes.reduce(...toMapById),
},
});
const [
{
application: { capabilities },
@ -1034,46 +1040,48 @@ describe('rule_form', () => {
let wrapper: ReactWrapper<any>;
async function setup() {
const { useLoadRuleTypes } = jest.requireMock('../../hooks/use_load_rule_types');
useLoadRuleTypes.mockReturnValue({
ruleTypes: [
{
id: 'other-consumer-producer-rule-type',
name: 'Test',
actionGroups: [
{
id: 'testActionGroup',
name: 'Test Action Group',
const { useLoadRuleTypesQuery } = jest.requireMock('../../hooks/use_load_rule_types_query');
useLoadRuleTypesQuery.mockReturnValue({
ruleTypesState: {
data: [
{
id: 'other-consumer-producer-rule-type',
name: 'Test',
actionGroups: [
{
id: 'testActionGroup',
name: 'Test Action Group',
},
],
defaultActionGroupId: 'testActionGroup',
minimumLicenseRequired: 'basic',
recoveryActionGroup: RecoveredActionGroup,
producer: ALERTS_FEATURE_ID,
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: true },
test: { read: true, all: true },
},
],
defaultActionGroupId: 'testActionGroup',
minimumLicenseRequired: 'basic',
recoveryActionGroup: RecoveredActionGroup,
producer: ALERTS_FEATURE_ID,
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: true },
test: { read: true, all: true },
},
},
{
id: 'same-consumer-producer-rule-type',
name: 'Test',
actionGroups: [
{
id: 'testActionGroup',
name: 'Test Action Group',
{
id: 'same-consumer-producer-rule-type',
name: 'Test',
actionGroups: [
{
id: 'testActionGroup',
name: 'Test Action Group',
},
],
defaultActionGroupId: 'testActionGroup',
minimumLicenseRequired: 'basic',
recoveryActionGroup: RecoveredActionGroup,
producer: 'test',
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: true },
test: { read: true, all: true },
},
],
defaultActionGroupId: 'testActionGroup',
minimumLicenseRequired: 'basic',
recoveryActionGroup: RecoveredActionGroup,
producer: 'test',
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: true },
test: { read: true, all: true },
},
},
],
].reduce(...toMapById),
},
});
const mocks = coreMock.createSetup();
const [
@ -1152,7 +1160,7 @@ describe('rule_form', () => {
wrapper.update();
});
expect(useLoadRuleTypes).toHaveBeenCalled();
expect(useLoadRuleTypesQuery).toHaveBeenCalled();
}
it('renders rule type options which producer correspond to the rule consumer', async () => {
@ -1198,7 +1206,7 @@ describe('rule_form', () => {
minimumScheduleInterval: { value: '1m', enforce: false },
}}
dispatch={() => {}}
errors={{ name: [], 'schedule.interval': [], ruleTypeId: [] }}
errors={{ name: [], 'schedule.interval': [], ruleTypeId: [], actionConnectors: [] }}
operation="create"
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}

View file

@ -90,8 +90,8 @@ import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants';
import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../../constants';
import { SectionLoading } from '../../components/section_loading';
import { RuleFormConsumerSelection, VALID_CONSUMERS } from './rule_form_consumer_selection';
import { useLoadRuleTypes } from '../../hooks/use_load_rule_types';
import { getInitialInterval } from './get_initial_interval';
import { useLoadRuleTypesQuery } from '../../hooks/use_load_rule_types_query';
const ENTER_KEY = 13;
@ -159,6 +159,8 @@ interface RuleFormProps<MetaData = Record<string, any>> {
useRuleProducer?: boolean;
}
const EMPTY_ARRAY: string[] = [];
export const RuleForm = ({
rule,
config,
@ -174,7 +176,7 @@ export const RuleForm = ({
ruleTypeRegistry,
actionTypeRegistry,
metadata,
filteredRuleTypes: ruleTypeToFilter,
filteredRuleTypes: ruleTypeToFilter = EMPTY_ARRAY,
hideGrouping = false,
hideInterval,
connectorFeatureId = AlertingConnectorFeatureId,
@ -221,11 +223,13 @@ export const RuleForm = ({
const [solutionsFilter, setSolutionFilter] = useState<string[]>([]);
let hasDisabledByLicenseRuleTypes: boolean = false;
const {
ruleTypes,
error: loadRuleTypesError,
ruleTypeIndex,
ruleTypesIsLoading,
} = useLoadRuleTypes({ filteredRuleTypes: ruleTypeToFilter });
ruleTypesState: {
data: ruleTypeIndex,
error: loadRuleTypesError,
isLoading: ruleTypesIsLoading,
},
} = useLoadRuleTypesQuery({ filteredRuleTypes: ruleTypeToFilter });
const ruleTypes = useMemo(() => [...ruleTypeIndex.values()], [ruleTypeIndex]);
// load rule types
useEffect(() => {
@ -961,94 +965,98 @@ export const RuleForm = ({
</EuiFlexItem>
</EuiFlexGrid>
<EuiSpacer size="m" />
{ruleTypeModel ? (
<>{ruleTypeDetails}</>
) : availableRuleTypes.length ? (
<>
<EuiHorizontalRule />
<EuiFormRow
fullWidth
labelAppend={
hasDisabledByLicenseRuleTypes && (
<div data-test-subj="ruleGroupTypeSelectContainer">
{ruleTypeModel ? (
<>{ruleTypeDetails}</>
) : availableRuleTypes.length ? (
<>
<EuiHorizontalRule />
<EuiFormRow
fullWidth
labelAppend={
hasDisabledByLicenseRuleTypes && (
<EuiTitle size="xxs">
<EuiLink
href={VIEW_LICENSE_OPTIONS_LINK}
target="_blank"
external
className="actActionForm__getMoreActionsLink"
>
<FormattedMessage
defaultMessage="Get more rule types"
id="xpack.triggersActionsUI.sections.actionForm.getMoreRuleTypesTitle"
/>
</EuiLink>
</EuiTitle>
)
}
label={
<EuiTitle size="xxs">
<EuiLink
href={VIEW_LICENSE_OPTIONS_LINK}
target="_blank"
external
className="actActionForm__getMoreActionsLink"
>
<h5>
<FormattedMessage
defaultMessage="Get more rule types"
id="xpack.triggersActionsUI.sections.actionForm.getMoreRuleTypesTitle"
id="xpack.triggersActionsUI.sections.ruleForm.ruleTypeSelectLabel"
defaultMessage="Select rule type"
/>
</EuiLink>
</h5>
</EuiTitle>
)
}
label={
<EuiTitle size="xxs">
<h5>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleForm.ruleTypeSelectLabel"
defaultMessage="Select rule type"
/>
</h5>
</EuiTitle>
}
>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFieldSearch
fullWidth
data-test-subj="ruleSearchField"
onChange={(e) => {
setInputText(e.target.value);
if (e.target.value === '') {
setSearchText('');
}
}}
onKeyUp={(e) => {
if (e.keyCode === ENTER_KEY) {
setSearchText(inputText);
}
}}
placeholder={i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.searchPlaceholderTitle',
{ defaultMessage: 'Search' }
)}
/>
</EuiFlexItem>
{solutions ? (
<EuiFlexItem grow={false}>
<SolutionFilter
key="solution-filter"
solutions={solutions}
onChange={(selectedSolutions: string[]) => setSolutionFilter(selectedSolutions)}
}
>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFieldSearch
fullWidth
data-test-subj="ruleSearchField"
onChange={(e) => {
setInputText(e.target.value);
if (e.target.value === '') {
setSearchText('');
}
}}
onKeyUp={(e) => {
if (e.keyCode === ENTER_KEY) {
setSearchText(inputText);
}
}}
placeholder={i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.searchPlaceholderTitle',
{ defaultMessage: 'Search' }
)}
/>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiFormRow>
<EuiSpacer />
{errors.ruleTypeId.length >= 1 && rule.ruleTypeId !== undefined ? (
<>
<EuiSpacer />
<EuiCallOut color="danger" size="s" title={errors.ruleTypeId} />
<EuiSpacer />
</>
) : null}
{ruleTypeNodes}
</>
) : ruleTypeIndex && !ruleTypesIsLoading ? (
<NoAuthorizedRuleTypes operation={operation} />
) : (
<SectionLoading>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleForm.loadingRuleTypesDescription"
defaultMessage="Loading rule types…"
/>
</SectionLoading>
)}
{solutions ? (
<EuiFlexItem grow={false}>
<SolutionFilter
key="solution-filter"
solutions={solutions}
onChange={(selectedSolutions: string[]) =>
setSolutionFilter(selectedSolutions)
}
/>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiFormRow>
<EuiSpacer />
{errors.ruleTypeId.length >= 1 && rule.ruleTypeId !== undefined ? (
<>
<EuiSpacer />
<EuiCallOut color="danger" size="s" title={errors.ruleTypeId} />
<EuiSpacer />
</>
) : null}
{ruleTypeNodes}
</>
) : ruleTypeIndex && !ruleTypesIsLoading ? (
<NoAuthorizedRuleTypes operation={operation} />
) : (
<SectionLoading>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleForm.loadingRuleTypesDescription"
defaultMessage="Loading rule types…"
/>
</SectionLoading>
)}
</div>
</EuiForm>
);
};

View file

@ -6,16 +6,20 @@
*/
import React from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ConnectorProvider } from '../application/context/connector_context';
import { RuleAdd } from '../application/sections/rule_form';
import type { ConnectorServices, RuleAddProps } from '../types';
import { queryClient } from '../application/query_client';
export const getAddRuleFlyoutLazy = (
props: RuleAddProps & { connectorServices: ConnectorServices }
) => {
return (
<ConnectorProvider value={{ services: props.connectorServices }}>
<RuleAdd {...props} />
<QueryClientProvider client={queryClient}>
<RuleAdd {...props} />
</QueryClientProvider>
</ConnectorProvider>
);
};

View file

@ -125,7 +125,7 @@ export {
deprecatedMessage,
} from './common';
export { useLoadRuleTypes, useSubAction } from './application/hooks';
export { useLoadRuleTypesQuery, useSubAction } from './application/hooks';
export type {
TriggersAndActionsUIPublicPluginSetup,