mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[RAM] Add simplified rules table to connector flyout (#176456)
## Summary
Resolves: https://github.com/elastic/response-ops-team/issues/174
Adds a rules table to the edit connector flyout view to let the user see
what rules are associated with said connector. The user can click on a
rule name to bring them to the rules detail page.

### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
7424285ab6
commit
20cecfcd3d
15 changed files with 685 additions and 100 deletions
12
x-pack/plugins/alerting/common/action_ref_prefix.ts
Normal file
12
x-pack/plugins/alerting/common/action_ref_prefix.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
|
||||
export const preconfiguredConnectorActionRefPrefix = 'preconfigured:';
|
||||
|
||||
// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
|
||||
export const systemConnectorActionRefPrefix = 'system_action:';
|
|
@ -39,6 +39,7 @@ export * from './iso_weekdays';
|
|||
export * from './saved_objects/rules/mappings';
|
||||
export * from './rule_circuit_breaker_error_message';
|
||||
export * from './maintenance_window_scoped_query_error_message';
|
||||
export * from './action_ref_prefix';
|
||||
|
||||
export type {
|
||||
MaintenanceWindowModificationMetadata,
|
||||
|
|
|
@ -10,15 +10,14 @@ import {
|
|||
AlertingAuthorizationFilterOpts,
|
||||
} from '../../authorization';
|
||||
|
||||
export {
|
||||
systemConnectorActionRefPrefix,
|
||||
preconfiguredConnectorActionRefPrefix,
|
||||
} from '../../../common';
|
||||
|
||||
// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
|
||||
export const extractedSavedObjectParamReferenceNamePrefix = 'param:';
|
||||
|
||||
// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
|
||||
export const preconfiguredConnectorActionRefPrefix = 'preconfigured:';
|
||||
|
||||
// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
|
||||
export const systemConnectorActionRefPrefix = 'system_action:';
|
||||
|
||||
export const alertingAuthorizationFilterOpts: AlertingAuthorizationFilterOpts = {
|
||||
type: AlertingAuthorizationFilterType.KQL,
|
||||
fieldNames: { ruleTypeId: 'alert.attributes.alertTypeId', consumer: 'alert.attributes.consumer' },
|
||||
|
|
|
@ -21,6 +21,7 @@ import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
|
|||
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import { PluginStartContract as AlertingStart } from '@kbn/alerting-plugin/public';
|
||||
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
|
@ -37,6 +38,7 @@ import { setDataViewsService } from '../common/lib/data_apis';
|
|||
import { KibanaContextProvider, useKibana } from '../common/lib/kibana';
|
||||
import { ConnectorProvider } from './context/connector_context';
|
||||
import { Section } from './constants';
|
||||
import { queryClient } from './query_client';
|
||||
|
||||
const ActionsConnectorsHome = lazy(
|
||||
() => import('./sections/actions_connectors_list/components/actions_connectors_home')
|
||||
|
@ -85,7 +87,9 @@ export const App = ({ deps }: { deps: TriggersAndActionsUiServices }) => {
|
|||
<KibanaThemeProvider theme$={theme.theme$}>
|
||||
<KibanaContextProvider services={{ ...deps }}>
|
||||
<Router history={deps.history}>
|
||||
<AppWithoutRouter sectionsRegex={sectionsRegex} />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppWithoutRouter sectionsRegex={sectionsRegex} />
|
||||
</QueryClientProvider>
|
||||
</Router>
|
||||
</KibanaContextProvider>
|
||||
</KibanaThemeProvider>
|
||||
|
|
|
@ -50,6 +50,8 @@ export enum SORT_ORDERS {
|
|||
|
||||
export const DEFAULT_SEARCH_PAGE_SIZE: number = 10;
|
||||
|
||||
export const DEFAULT_CONNECTOR_RULES_LIST_PAGE_SIZE: number = 25;
|
||||
|
||||
export const DEFAULT_RULE_INTERVAL = '1m';
|
||||
|
||||
export const RULE_EXECUTION_LOG_COLUMN_IDS = [
|
||||
|
|
|
@ -21,10 +21,14 @@ type UseLoadRulesQueryProps = Omit<LoadRulesProps, 'http'> & {
|
|||
enabled: boolean;
|
||||
refresh?: Date;
|
||||
filterConsumers?: string[];
|
||||
hasReference?: {
|
||||
type: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const useLoadRulesQuery = (props: UseLoadRulesQueryProps) => {
|
||||
const { filterConsumers, filters, page, sort, onPage, enabled, refresh } = props;
|
||||
const { filterConsumers, filters, page, sort, onPage, enabled, refresh, hasReference } = props;
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
|
@ -53,6 +57,7 @@ export const useLoadRulesQuery = (props: UseLoadRulesQueryProps) => {
|
|||
refresh: refresh?.toISOString(),
|
||||
},
|
||||
filterConsumers,
|
||||
hasReference,
|
||||
],
|
||||
queryFn: () => {
|
||||
return loadRulesWithKueryFilter({
|
||||
|
@ -69,6 +74,7 @@ export const useLoadRulesQuery = (props: UseLoadRulesQueryProps) => {
|
|||
kueryNode: filters.kueryNode,
|
||||
sort,
|
||||
filterConsumers,
|
||||
hasReference,
|
||||
});
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
|
|
|
@ -25,6 +25,10 @@ export interface LoadRulesProps {
|
|||
sort?: Sorting;
|
||||
kueryNode?: KueryNode;
|
||||
filterConsumers?: string[];
|
||||
hasReference?: {
|
||||
type: string;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const rewriteRulesResponseRes = (results: Array<AsApiContract<Rule>>): Rule[] => {
|
||||
|
|
|
@ -25,6 +25,7 @@ export async function loadRulesWithKueryFilter({
|
|||
sort = { field: 'name', direction: 'asc' },
|
||||
kueryNode,
|
||||
filterConsumers,
|
||||
hasReference,
|
||||
}: LoadRulesProps): Promise<{
|
||||
page: number;
|
||||
perPage: number;
|
||||
|
@ -58,6 +59,7 @@ export async function loadRulesWithKueryFilter({
|
|||
sort_field: sort.field,
|
||||
sort_order: sort.direction,
|
||||
filter_consumers: filterConsumers,
|
||||
...(hasReference ? { has_reference: hasReference } : {}),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { fromKueryExpression } from '@kbn/es-query';
|
||||
import type { IToasts } from '@kbn/core/public';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../common/get_experimental_features';
|
||||
import { ConnectorRulesList } from './connector_rules_list';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import {} from '../../lib/rule_api/rules_kuery_filter';
|
||||
import { ActionConnector } from '../../../types';
|
||||
import { mockedRulesData, ruleTypeFromApi } from '../rules_list/components/test_helpers';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('../../lib/rule_api/rules_kuery_filter', () => ({
|
||||
loadRulesWithKueryFilter: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../lib/rule_api/rule_types', () => ({
|
||||
loadRuleTypes: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../common/get_experimental_features', () => ({
|
||||
getIsExperimentalFeatureEnabled: jest.fn(),
|
||||
}));
|
||||
|
||||
const { loadRuleTypes } = jest.requireMock('../../lib/rule_api/rule_types');
|
||||
const { loadRulesWithKueryFilter } = jest.requireMock('../../lib/rule_api/rules_kuery_filter');
|
||||
|
||||
const getUrlForAppMock = jest.fn();
|
||||
const addSuccessMock = jest.fn();
|
||||
const addErrorMock = jest.fn();
|
||||
const addDangerMock = jest.fn();
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
cacheTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('Connector rules list', () => {
|
||||
beforeAll(() => {
|
||||
(getIsExperimentalFeatureEnabled as jest.Mock<any, any>).mockImplementation(() => false);
|
||||
useKibanaMock().services.application.getUrlForApp = getUrlForAppMock;
|
||||
useKibanaMock().services.notifications.toasts = {
|
||||
addSuccessMock,
|
||||
addErrorMock,
|
||||
addDangerMock,
|
||||
} as unknown as IToasts;
|
||||
loadRulesWithKueryFilter.mockResolvedValue({
|
||||
page: 1,
|
||||
perPage: 25,
|
||||
total: mockedRulesData.length,
|
||||
data: mockedRulesData,
|
||||
});
|
||||
loadRuleTypes.mockResolvedValue([ruleTypeFromApi]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ConnectorRulesList
|
||||
connector={
|
||||
{
|
||||
id: 'test-id',
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false,
|
||||
} as ActionConnector
|
||||
}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('connectorRulesList')).toBeInTheDocument();
|
||||
expect(screen.queryAllByTestId('connectorRuleRow')).toHaveLength(mockedRulesData.length);
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow for sorting by name', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ConnectorRulesList
|
||||
connector={
|
||||
{
|
||||
id: 'test-id',
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false,
|
||||
} as ActionConnector
|
||||
}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('connectorRuleRow')).toHaveLength(mockedRulesData.length);
|
||||
});
|
||||
|
||||
const nameColumnTableHeaderEl = await screen.findByTestId('tableHeaderCell_name_0');
|
||||
|
||||
const el = nameColumnTableHeaderEl.querySelector(
|
||||
'[data-test-subj="tableHeaderCell_name_0"] .euiTableHeaderButton'
|
||||
) as HTMLElement;
|
||||
|
||||
fireEvent.click(el);
|
||||
|
||||
expect(loadRulesWithKueryFilter).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
sort: { direction: 'desc', field: 'name' },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow for searching by text', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ConnectorRulesList
|
||||
connector={
|
||||
{
|
||||
id: 'test-id',
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false,
|
||||
} as ActionConnector
|
||||
}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('connectorRuleRow')).toHaveLength(mockedRulesData.length);
|
||||
});
|
||||
|
||||
userEvent.type(screen.getByTestId('connectorRulesListSearch'), 'test{enter}');
|
||||
|
||||
expect(loadRulesWithKueryFilter).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
searchText: 'test',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should find preconfigured rules correctly', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ConnectorRulesList
|
||||
connector={
|
||||
{
|
||||
id: 'test-id',
|
||||
isPreconfigured: true,
|
||||
isSystemAction: false,
|
||||
} as ActionConnector
|
||||
}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('connectorRuleRow')).toHaveLength(mockedRulesData.length);
|
||||
});
|
||||
|
||||
expect(loadRulesWithKueryFilter).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
hasReference: undefined,
|
||||
kueryNode: fromKueryExpression(
|
||||
`alert.attributes.actions:{ actionRef: "preconfigured:test-id" }`
|
||||
),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should find system actions rules correctly', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ConnectorRulesList
|
||||
connector={
|
||||
{
|
||||
id: 'test-id',
|
||||
isPreconfigured: false,
|
||||
isSystemAction: true,
|
||||
} as ActionConnector
|
||||
}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('connectorRuleRow')).toHaveLength(mockedRulesData.length);
|
||||
});
|
||||
|
||||
expect(loadRulesWithKueryFilter).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
hasReference: undefined,
|
||||
kueryNode: fromKueryExpression(
|
||||
`alert.attributes.actions:{ actionRef: "system_action:test-id" }`
|
||||
),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,272 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState, useMemo, ChangeEvent } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getRuleDetailsRoute } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiTableSortingType,
|
||||
EuiLink,
|
||||
EuiText,
|
||||
EuiHealth,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFieldSearch,
|
||||
} from '@elastic/eui';
|
||||
import { fromKueryExpression } from '@kbn/es-query';
|
||||
import {
|
||||
systemConnectorActionRefPrefix,
|
||||
preconfiguredConnectorActionRefPrefix,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { getRuleHealthColor } from '../../../common/lib/rule_status_helpers';
|
||||
import { useLoadRuleTypesQuery } from '../../hooks/use_load_rule_types_query';
|
||||
import { useLoadRulesQuery } from '../../hooks/use_load_rules_query';
|
||||
import { Pagination, Rule, ActionConnector } from '../../../types';
|
||||
import { DEFAULT_CONNECTOR_RULES_LIST_PAGE_SIZE } from '../../constants';
|
||||
import { rulesLastRunOutcomeTranslationMapping } from '../rules_list/translations';
|
||||
import { NoPermissionPrompt } from '../../components/prompts/no_permission_prompt';
|
||||
import { CenterJustifiedSpinner } from '../../components/center_justified_spinner';
|
||||
import { RuleTagBadge } from '../rules_list/components/rule_tag_badge';
|
||||
|
||||
export interface ConnectorRulesListProps {
|
||||
connector: ActionConnector;
|
||||
}
|
||||
|
||||
export const ConnectorRulesList = (props: ConnectorRulesListProps) => {
|
||||
const { connector } = props;
|
||||
|
||||
const {
|
||||
application: { getUrlForApp },
|
||||
} = useKibana().services;
|
||||
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [searchFilter, setSearchFilter] = useState<string>('');
|
||||
const [tagPopoverOpenId, setTagPopoverOpenId] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState<Pagination>({
|
||||
index: 0,
|
||||
size: DEFAULT_CONNECTOR_RULES_LIST_PAGE_SIZE,
|
||||
});
|
||||
const [sort, setSort] = useState<EuiTableSortingType<Rule>['sort']>({
|
||||
field: 'name',
|
||||
direction: 'asc',
|
||||
});
|
||||
|
||||
const {
|
||||
ruleTypesState,
|
||||
hasAnyAuthorizedRuleType,
|
||||
authorizedRuleTypes,
|
||||
authorizedToReadAnyRules,
|
||||
isSuccess: isLoadRuleTypesSuccess,
|
||||
} = useLoadRuleTypesQuery({ filteredRuleTypes: [] });
|
||||
|
||||
const ruleTypeIds = authorizedRuleTypes.map((art) => art.id);
|
||||
|
||||
const ruleFilters = useMemo(() => {
|
||||
const baseFilters = {
|
||||
searchText: searchFilter,
|
||||
types: ruleTypeIds,
|
||||
};
|
||||
|
||||
if (connector.isPreconfigured) {
|
||||
return {
|
||||
filters: {
|
||||
...baseFilters,
|
||||
kueryNode: fromKueryExpression(
|
||||
`alert.attributes.actions:{ actionRef: "${preconfiguredConnectorActionRefPrefix}${connector.id}" }`
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (connector.isSystemAction) {
|
||||
return {
|
||||
filters: {
|
||||
...baseFilters,
|
||||
kueryNode: fromKueryExpression(
|
||||
`alert.attributes.actions:{ actionRef: "${systemConnectorActionRefPrefix}${connector.id}" }`
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
filters: baseFilters,
|
||||
hasReference: {
|
||||
type: 'action',
|
||||
id: connector.id,
|
||||
},
|
||||
};
|
||||
}, [connector, searchFilter, ruleTypeIds]);
|
||||
|
||||
const { rulesState } = useLoadRulesQuery({
|
||||
...ruleFilters,
|
||||
hasDefaultRuleTypesFiltersOn: ruleTypeIds.length === 0,
|
||||
page,
|
||||
sort,
|
||||
onPage: setPage,
|
||||
enabled: isLoadRuleTypesSuccess && hasAnyAuthorizedRuleType,
|
||||
});
|
||||
|
||||
const showNoAuthPrompt = !ruleTypesState.initialLoad && !authorizedToReadAnyRules;
|
||||
const isInitialLoading = ruleTypesState.initialLoad || rulesState.initialLoad;
|
||||
const showSpinner =
|
||||
isInitialLoading && (ruleTypesState.isLoading || (!showNoAuthPrompt && rulesState.isLoading));
|
||||
const isLoading = ruleTypesState.isLoading || rulesState.isLoading;
|
||||
|
||||
const onSetTagPopoverOpenId = useCallback(
|
||||
(id: string | null) => () => {
|
||||
setTagPopoverOpenId(id);
|
||||
},
|
||||
[setTagPopoverOpenId]
|
||||
);
|
||||
|
||||
const columns = useMemo<Array<EuiBasicTableColumn<Rule>>>(() => {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate('xpack.triggersActionsUI.sections.connectorRulesList.columns.name', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '33%',
|
||||
render: (name: string, rule: Rule) => {
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
title={name}
|
||||
href={getUrlForApp('management', {
|
||||
path: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(rule.id)}`,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued" size="xs">
|
||||
{ruleTypesState.data.get(rule.ruleTypeId)?.name || rule.ruleTypeId}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'tags',
|
||||
name: i18n.translate('xpack.triggersActionsUI.sections.connectorRulesList.columns.tags', {
|
||||
defaultMessage: 'Tags',
|
||||
}),
|
||||
width: '50px',
|
||||
sortable: false,
|
||||
render: (ruleTags: string[], rule: Rule) => {
|
||||
return ruleTags.length > 0 ? (
|
||||
<RuleTagBadge
|
||||
isOpen={tagPopoverOpenId === rule.id}
|
||||
tags={ruleTags}
|
||||
onClick={onSetTagPopoverOpenId(rule.id)}
|
||||
onClose={onSetTagPopoverOpenId(null)}
|
||||
/>
|
||||
) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'lastRun.outcome',
|
||||
name: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorRulesList.columns.lastResponse',
|
||||
{ defaultMessage: 'Last response' }
|
||||
),
|
||||
width: '150px',
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
align: 'right',
|
||||
render: (_, rule: Rule) => {
|
||||
return (
|
||||
rule.lastRun && (
|
||||
<EuiHealth color={getRuleHealthColor(rule) || 'default'}>
|
||||
{rulesLastRunOutcomeTranslationMapping[rule.lastRun.outcome]}
|
||||
</EuiHealth>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [ruleTypesState, tagPopoverOpenId, getUrlForApp, onSetTagPopoverOpenId]);
|
||||
|
||||
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchText(e.target.value);
|
||||
}, []);
|
||||
|
||||
const onSearch = useCallback(() => {
|
||||
setSearchFilter(searchText);
|
||||
}, [searchText]);
|
||||
|
||||
if (showNoAuthPrompt) {
|
||||
return <NoPermissionPrompt />;
|
||||
}
|
||||
|
||||
if (showSpinner) {
|
||||
return <CenterJustifiedSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFieldSearch
|
||||
data-test-subj="connectorRulesListSearch"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorRulesList.fieldSearch.label',
|
||||
{ defaultMessage: 'Search rules by name and tags' }
|
||||
)}
|
||||
fullWidth
|
||||
incremental={false}
|
||||
onChange={onChange}
|
||||
onSearch={onSearch}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorRulesList.fieldSearch.placeholder',
|
||||
{ defaultMessage: 'Search rules by name and tags' }
|
||||
)}
|
||||
value={searchText}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiBasicTable
|
||||
data-test-subj="connectorRulesList"
|
||||
loading={isLoading}
|
||||
items={rulesState.data}
|
||||
columns={columns}
|
||||
sorting={{ sort }}
|
||||
pagination={{
|
||||
pageIndex: page.index,
|
||||
pageSize: page.size,
|
||||
totalItemCount: ruleTypesState.initialLoad ? 0 : rulesState.totalItemCount,
|
||||
}}
|
||||
onChange={({
|
||||
page: changedPage,
|
||||
sort: changedSort,
|
||||
}: {
|
||||
page?: Pagination;
|
||||
sort?: EuiTableSortingType<Rule>['sort'];
|
||||
}) => {
|
||||
if (changedPage) {
|
||||
setPage(changedPage);
|
||||
}
|
||||
if (changedSort) {
|
||||
setSort(changedSort);
|
||||
}
|
||||
}}
|
||||
rowProps={{ 'data-test-subj': 'connectorRuleRow' }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import {
|
||||
EuiBetaBadge,
|
||||
|
@ -35,7 +35,7 @@ const FlyoutHeaderComponent: React.FC<{
|
|||
connectorTypeId: string;
|
||||
connectorTypeDesc: string;
|
||||
selectedTab: EditConnectorTabs;
|
||||
setTab: () => void;
|
||||
setTab: (nextPage: EditConnectorTabs) => void;
|
||||
icon?: IconType | null;
|
||||
}> = ({
|
||||
icon,
|
||||
|
@ -55,6 +55,18 @@ const FlyoutHeaderComponent: React.FC<{
|
|||
const { euiTheme } = useEuiTheme();
|
||||
const canExecute = hasExecuteActionsCapability(capabilities, connectorTypeId);
|
||||
|
||||
const setConfigurationTab = useCallback(() => {
|
||||
setTab(EditConnectorTabs.Configuration);
|
||||
}, [setTab]);
|
||||
|
||||
const setTestTab = useCallback(() => {
|
||||
setTab(EditConnectorTabs.Test);
|
||||
}, [setTab]);
|
||||
|
||||
const setRulesTab = useCallback(() => {
|
||||
setTab(EditConnectorTabs.Rules);
|
||||
}, [setTab]);
|
||||
|
||||
return (
|
||||
<EuiFlyoutHeader hasBorder data-test-subj="edit-connector-flyout-header">
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
|
@ -153,7 +165,7 @@ const FlyoutHeaderComponent: React.FC<{
|
|||
`}
|
||||
>
|
||||
<EuiTab
|
||||
onClick={setTab}
|
||||
onClick={setConfigurationTab}
|
||||
data-test-subj="configureConnectorTab"
|
||||
isSelected={EditConnectorTabs.Configuration === selectedTab}
|
||||
>
|
||||
|
@ -161,9 +173,18 @@ const FlyoutHeaderComponent: React.FC<{
|
|||
defaultMessage: 'Configuration',
|
||||
})}
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
onClick={setRulesTab}
|
||||
data-test-subj="rulesConnectorTab"
|
||||
isSelected={EditConnectorTabs.Rules === selectedTab}
|
||||
>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.rulesConnectorList.tabText', {
|
||||
defaultMessage: 'Rules',
|
||||
})}
|
||||
</EuiTab>
|
||||
{canExecute && (
|
||||
<EuiTab
|
||||
onClick={setTab}
|
||||
onClick={setTestTab}
|
||||
data-test-subj="testConnectorTab"
|
||||
isSelected={EditConnectorTabs.Test === selectedTab}
|
||||
>
|
||||
|
|
|
@ -24,7 +24,8 @@ import type { ConnectorFormSchema } from '../types';
|
|||
import { useUpdateConnector } from '../../../hooks/use_edit_connector';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { hasSaveActionsCapability } from '../../../lib/capabilities';
|
||||
import TestConnectorForm from '../test_connector_form';
|
||||
import { TestConnectorForm } from '../test_connector_form';
|
||||
import { ConnectorRulesList } from '../connector_rules_list';
|
||||
import { useExecuteConnector } from '../../../hooks/use_execute_connector';
|
||||
import { FlyoutHeader } from './header';
|
||||
import { FlyoutFooter } from './footer';
|
||||
|
@ -85,19 +86,13 @@ const EditConnectorFlyoutComponent: React.FC<EditConnectorFlyoutProps> = ({
|
|||
useState<Option<ActionTypeExecutorResult<unknown> | undefined>>(none);
|
||||
|
||||
const handleSetTab = useCallback(
|
||||
() =>
|
||||
setTab((prevTab) => {
|
||||
if (prevTab === EditConnectorTabs.Configuration) {
|
||||
return EditConnectorTabs.Test;
|
||||
}
|
||||
|
||||
if (testExecutionResult !== none) {
|
||||
setTestExecutionResult(none);
|
||||
}
|
||||
|
||||
return EditConnectorTabs.Configuration;
|
||||
}),
|
||||
[testExecutionResult]
|
||||
(nextPage: EditConnectorTabs) => {
|
||||
if (nextPage === EditConnectorTabs.Configuration && testExecutionResult !== none) {
|
||||
setTestExecutionResult(none);
|
||||
}
|
||||
setTab(nextPage);
|
||||
},
|
||||
[testExecutionResult, setTestExecutionResult]
|
||||
);
|
||||
|
||||
const [isFormModified, setIsFormModified] = useState<boolean>(false);
|
||||
|
@ -217,6 +212,99 @@ const EditConnectorFlyoutComponent: React.FC<EditConnectorFlyoutProps> = ({
|
|||
};
|
||||
}, []);
|
||||
|
||||
const renderConfigurationTab = useCallback(() => {
|
||||
if (!connector.isPreconfigured && !connector.isSystemAction) {
|
||||
return (
|
||||
<>
|
||||
{isEdit && (
|
||||
<>
|
||||
<ConnectorForm
|
||||
actionTypeModel={actionTypeModel}
|
||||
connector={getConnectorWithoutSecrets(connector)}
|
||||
isEdit={isEdit}
|
||||
onChange={setFormState}
|
||||
onFormModifiedChange={onFormModifiedChange}
|
||||
/>
|
||||
{!!preSubmitValidationErrorMessage && <p>{preSubmitValidationErrorMessage}</p>}
|
||||
{showButtons && (
|
||||
<EuiButton
|
||||
fill
|
||||
iconType={isSaved ? 'check' : undefined}
|
||||
color="success"
|
||||
data-test-subj="edit-connector-flyout-save-btn"
|
||||
isLoading={isSaving}
|
||||
onClick={onClickSave}
|
||||
disabled={!isFormModified || hasErrors || isSaving}
|
||||
>
|
||||
{isSaved ? (
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.editConnectorForm.saveButtonSavedLabel"
|
||||
defaultMessage="Changes Saved"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.editConnectorForm.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
)}
|
||||
</EuiButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ReadOnlyConnectorMessage
|
||||
href={docLinks.links.alerting.preconfiguredConnectors}
|
||||
extraComponent={actionTypeModel?.actionReadOnlyExtraComponent}
|
||||
connectorId={connector.id}
|
||||
connectorName={connector.name}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
connector,
|
||||
actionTypeModel,
|
||||
isEdit,
|
||||
docLinks.links.alerting.preconfiguredConnectors,
|
||||
hasErrors,
|
||||
isFormModified,
|
||||
isSaved,
|
||||
isSaving,
|
||||
preSubmitValidationErrorMessage,
|
||||
showButtons,
|
||||
onClickSave,
|
||||
onFormModifiedChange,
|
||||
]);
|
||||
|
||||
const renderTestTab = useCallback(() => {
|
||||
return (
|
||||
<TestConnectorForm
|
||||
connector={connector}
|
||||
executeEnabled={!isFormModified}
|
||||
actionParams={testExecutionActionParams}
|
||||
setActionParams={setTestExecutionActionParams}
|
||||
onExecutionAction={onExecutionAction}
|
||||
isExecutingAction={isExecutingConnector}
|
||||
executionResult={testExecutionResult}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
connector,
|
||||
actionTypeRegistry,
|
||||
isExecutingConnector,
|
||||
isFormModified,
|
||||
testExecutionActionParams,
|
||||
testExecutionResult,
|
||||
onExecutionAction,
|
||||
]);
|
||||
|
||||
const renderConnectorRulesList = useCallback(() => {
|
||||
return <ConnectorRulesList connector={connector} />;
|
||||
}, [connector]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyout
|
||||
|
@ -237,65 +325,9 @@ const EditConnectorFlyoutComponent: React.FC<EditConnectorFlyoutProps> = ({
|
|||
isExperimental={actionTypeModel?.isExperimental}
|
||||
/>
|
||||
<EuiFlyoutBody>
|
||||
{selectedTab === EditConnectorTabs.Configuration ? (
|
||||
!connector.isPreconfigured && !connector.isSystemAction ? (
|
||||
<>
|
||||
{isEdit && (
|
||||
<>
|
||||
<ConnectorForm
|
||||
actionTypeModel={actionTypeModel}
|
||||
connector={getConnectorWithoutSecrets(connector)}
|
||||
isEdit={isEdit}
|
||||
onChange={setFormState}
|
||||
onFormModifiedChange={onFormModifiedChange}
|
||||
/>
|
||||
{!!preSubmitValidationErrorMessage && <p>{preSubmitValidationErrorMessage}</p>}
|
||||
{showButtons && (
|
||||
<EuiButton
|
||||
fill
|
||||
iconType={isSaved ? 'check' : undefined}
|
||||
color="success"
|
||||
data-test-subj="edit-connector-flyout-save-btn"
|
||||
isLoading={isSaving}
|
||||
onClick={onClickSave}
|
||||
disabled={!isFormModified || hasErrors || isSaving}
|
||||
>
|
||||
{isSaved ? (
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.editConnectorForm.saveButtonSavedLabel"
|
||||
defaultMessage="Changes Saved"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.editConnectorForm.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
)}
|
||||
</EuiButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<ReadOnlyConnectorMessage
|
||||
href={docLinks.links.alerting.preconfiguredConnectors}
|
||||
extraComponent={actionTypeModel?.actionReadOnlyExtraComponent}
|
||||
connectorId={connector.id}
|
||||
connectorName={connector.name}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<TestConnectorForm
|
||||
connector={connector}
|
||||
executeEnabled={!isFormModified}
|
||||
actionParams={testExecutionActionParams}
|
||||
setActionParams={setTestExecutionActionParams}
|
||||
onExecutionAction={onExecutionAction}
|
||||
isExecutingAction={isExecutingConnector}
|
||||
executionResult={testExecutionResult}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
/>
|
||||
)}
|
||||
{selectedTab === EditConnectorTabs.Configuration && renderConfigurationTab()}
|
||||
{selectedTab === EditConnectorTabs.Test && renderTestTab()}
|
||||
{selectedTab === EditConnectorTabs.Rules && renderConnectorRulesList()}
|
||||
</EuiFlyoutBody>
|
||||
<FlyoutFooter onClose={closeFlyout} />
|
||||
</EuiFlyout>
|
||||
|
|
|
@ -81,7 +81,7 @@ export const RulesListFiltersBar = React.memo((props: RulesListFiltersBarProps)
|
|||
isGrouped
|
||||
refresh={refresh}
|
||||
canLoadRules={canLoadRules}
|
||||
selectedTags={filters.tags}
|
||||
selectedTags={filters.tags || []}
|
||||
onChange={(value) => updateFilters({ filter: 'tags', value })}
|
||||
/>,
|
||||
];
|
||||
|
@ -93,7 +93,7 @@ export const RulesListFiltersBar = React.memo((props: RulesListFiltersBarProps)
|
|||
if (isRuleStatusFilterEnabled) {
|
||||
return (
|
||||
<RuleStatusFilter
|
||||
selectedStatuses={filters.ruleStatuses}
|
||||
selectedStatuses={filters.ruleStatuses || []}
|
||||
onChange={(value) => updateFilters({ filter: 'ruleStatuses', value })}
|
||||
/>
|
||||
);
|
||||
|
@ -106,7 +106,7 @@ export const RulesListFiltersBar = React.memo((props: RulesListFiltersBarProps)
|
|||
return [
|
||||
<RuleExecutionStatusFilter
|
||||
key="rule-status-filter"
|
||||
selectedStatuses={filters.ruleExecutionStatuses}
|
||||
selectedStatuses={filters.ruleExecutionStatuses || []}
|
||||
onChange={(value) => updateFilters({ filter: 'ruleExecutionStatuses', value })}
|
||||
/>,
|
||||
];
|
||||
|
@ -114,7 +114,7 @@ export const RulesListFiltersBar = React.memo((props: RulesListFiltersBarProps)
|
|||
return [
|
||||
<RuleLastRunOutcomeFilter
|
||||
key="rule-last-run-outcome-filter"
|
||||
selectedOutcomes={filters.ruleLastRunOutcomes}
|
||||
selectedOutcomes={filters.ruleLastRunOutcomes || []}
|
||||
onChange={(value) => updateFilters({ filter: 'ruleLastRunOutcomes', value })}
|
||||
/>,
|
||||
];
|
||||
|
@ -124,14 +124,14 @@ export const RulesListFiltersBar = React.memo((props: RulesListFiltersBarProps)
|
|||
<TypeFilter
|
||||
key="type-filter"
|
||||
options={filterOptions}
|
||||
filters={filters.types}
|
||||
filters={filters.types || []}
|
||||
onChange={(value) => updateFilters({ filter: 'types', value })}
|
||||
/>,
|
||||
showActionFilter && (
|
||||
<ActionTypeFilter
|
||||
key="action-type-filter"
|
||||
actionTypes={actionTypes}
|
||||
filters={filters.actionTypes}
|
||||
filters={filters.actionTypes || []}
|
||||
onChange={(value) => updateFilters({ filter: 'actionTypes', value })}
|
||||
/>
|
||||
),
|
||||
|
|
|
@ -6,17 +6,22 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ConnectorProvider } from '../application/context/connector_context';
|
||||
import { EditConnectorFlyout } from '../application/sections/action_connector_form';
|
||||
import { EditConnectorFlyoutProps } from '../application/sections/action_connector_form/edit_connector_flyout';
|
||||
import { ConnectorServices } from '../types';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export const getEditConnectorFlyoutLazy = (
|
||||
props: EditConnectorFlyoutProps & { connectorServices: ConnectorServices }
|
||||
) => {
|
||||
return (
|
||||
<ConnectorProvider value={{ services: props.connectorServices }}>
|
||||
<EditConnectorFlyout {...props} />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<EditConnectorFlyout {...props} />
|
||||
</QueryClientProvider>
|
||||
</ConnectorProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -425,6 +425,7 @@ export interface IErrorObject {
|
|||
export enum EditConnectorTabs {
|
||||
Configuration = 'configuration',
|
||||
Test = 'test',
|
||||
Rules = 'rules',
|
||||
}
|
||||
|
||||
export interface RuleEditProps<
|
||||
|
@ -817,14 +818,14 @@ export interface ConnectorServices {
|
|||
}
|
||||
|
||||
export interface RulesListFilters {
|
||||
actionTypes: string[];
|
||||
ruleExecutionStatuses: string[];
|
||||
ruleLastRunOutcomes: string[];
|
||||
ruleParams: Record<string, string | number | object>;
|
||||
ruleStatuses: RuleStatus[];
|
||||
searchText: string;
|
||||
tags: string[];
|
||||
types: string[];
|
||||
actionTypes?: string[];
|
||||
ruleExecutionStatuses?: string[];
|
||||
ruleLastRunOutcomes?: string[];
|
||||
ruleParams?: Record<string, string | number | object>;
|
||||
ruleStatuses?: RuleStatus[];
|
||||
searchText?: string;
|
||||
tags?: string[];
|
||||
types?: string[];
|
||||
kueryNode?: KueryNode;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue