mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
727fb131b5
commit
0f5b544b9e
18 changed files with 543 additions and 384 deletions
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 />);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -125,7 +125,7 @@ export {
|
|||
deprecatedMessage,
|
||||
} from './common';
|
||||
|
||||
export { useLoadRuleTypes, useSubAction } from './application/hooks';
|
||||
export { useLoadRuleTypesQuery, useSubAction } from './application/hooks';
|
||||
|
||||
export type {
|
||||
TriggersAndActionsUIPublicPluginSetup,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue