[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.


![connector_rules_list](edfe3b39-712f-423c-8ee0-89dbbe34e7bd)

### 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:
Jiawei Wu 2024-02-13 11:53:36 -08:00 committed by GitHub
parent 7424285ab6
commit 20cecfcd3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 685 additions and 100 deletions

View 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:';

View file

@ -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,

View file

@ -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' },

View file

@ -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>

View file

@ -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 = [

View file

@ -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) => {

View file

@ -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[] => {

View file

@ -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 } : {}),
}),
});

View file

@ -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" }`
),
})
);
});
});

View file

@ -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>
);
};

View file

@ -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}
>

View file

@ -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>

View file

@ -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 })}
/>
),

View file

@ -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>
);
};

View file

@ -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;
}