[RAM] Move Connectors to own page (#142485)

* Move Connectors to own page

* Move create connector button to header

* Update permissions IDs

* Fix some tests

* Test fixes

* Fix more tests

* Fix more tests

* Fix FTR 7

* Fix jest

* Fix FTR 4

* Fix FTR 6

* Fix translations

* Fix breadcrumbs

* Fix empty prompt design

* Update empty prompt text

* Update import warning url

* Add documentation buttons

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Zacqary Adam Xeper 2022-10-13 11:07:28 -05:00 committed by GitHub
parent 42a7a09267
commit ec98f01c9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 457 additions and 216 deletions

View file

@ -28,7 +28,7 @@ export const ACTIONS_FEATURE = {
app: [],
order: FEATURE_ORDER,
management: {
insightsAndAlerting: ['triggersActions'],
insightsAndAlerting: ['triggersActions', 'triggersActionsConnectors'],
},
privileges: {
all: {
@ -36,7 +36,7 @@ export const ACTIONS_FEATURE = {
api: [],
catalogue: [],
management: {
insightsAndAlerting: ['triggersActions'],
insightsAndAlerting: ['triggersActions', 'triggersActionsConnectors'],
},
savedObject: {
all: [
@ -53,7 +53,7 @@ export const ACTIONS_FEATURE = {
api: [],
catalogue: [],
management: {
insightsAndAlerting: ['triggersActions'],
insightsAndAlerting: ['triggersActions', 'triggersActionsConnectors'],
},
savedObject: {
// action execution requires 'read' over `actions`, but 'all' over `action_task_params`

View file

@ -29,7 +29,7 @@ export function getImportWarnings(
{
type: 'action_required',
message,
actionPath: '/app/management/insightsAndAlerting/triggersActions/connectors',
actionPath: '/app/management/insightsAndAlerting/triggersActionsConnectors',
buttonLabel: GO_TO_CONNECTORS_BUTTON_LABLE,
} as SavedObjectsImportWarning,
];

View file

@ -67,7 +67,7 @@ export class CasesUiPlugin
plugins.management.sections.section.insightsAndAlerting.registerApp({
id: APP_ID,
title: APP_TITLE,
order: 0,
order: 1,
async mount(params: ManagementAppMountParams) {
const [coreStart, pluginsStart] = (await core.getStartServices()) as [
CoreStart,

View file

@ -23,7 +23,7 @@ export function registerManagementSection(
title: i18n.translate('xpack.ml.management.jobsListTitle', {
defaultMessage: 'Machine Learning',
}),
order: 2,
order: 4,
async mount(params: ManagementAppMountParams) {
const { mountApp } = await import('./jobs_list');
return mountApp(core, params, deps);

View file

@ -172,7 +172,7 @@ export class ReportingPublicPlugin
management.sections.section.insightsAndAlerting.registerApp({
id: 'reporting',
title: this.title,
order: 1,
order: 3,
mount: async (params) => {
params.setBreadcrumbs([{ text: this.breadcrumbText }]);
const [[start, startDeps], { mountManagementSection }] = await Promise.all([

View file

@ -32297,7 +32297,6 @@
"xpack.triggersActionsUI.fieldBrowser.viewSelected": "sélectionné",
"xpack.triggersActionsUI.home.appTitle": "Règles et connecteurs",
"xpack.triggersActionsUI.home.breadcrumbTitle": "Règles et connecteurs",
"xpack.triggersActionsUI.home.connectorsTabTitle": "Connecteurs",
"xpack.triggersActionsUI.home.docsLinkText": "Documentation",
"xpack.triggersActionsUI.home.rulesTabTitle": "Règles",
"xpack.triggersActionsUI.home.sectionDescription": "Détecter les conditions à l'aide des règles, et entreprendre des actions à l'aide des connecteurs.",

View file

@ -32271,7 +32271,6 @@
"xpack.triggersActionsUI.fieldBrowser.viewSelected": "選択済み",
"xpack.triggersActionsUI.home.appTitle": "ルールとコネクター",
"xpack.triggersActionsUI.home.breadcrumbTitle": "ルールとコネクター",
"xpack.triggersActionsUI.home.connectorsTabTitle": "コネクター",
"xpack.triggersActionsUI.home.docsLinkText": "ドキュメント",
"xpack.triggersActionsUI.home.rulesTabTitle": "ルール",
"xpack.triggersActionsUI.home.sectionDescription": "ルールを使用して条件を検出し、コネクターを使用してアクションを実行します。",

View file

@ -32308,7 +32308,6 @@
"xpack.triggersActionsUI.fieldBrowser.viewSelected": "已选定",
"xpack.triggersActionsUI.home.appTitle": "规则和连接器",
"xpack.triggersActionsUI.home.breadcrumbTitle": "规则和连接器",
"xpack.triggersActionsUI.home.connectorsTabTitle": "连接器",
"xpack.triggersActionsUI.home.docsLinkText": "文档",
"xpack.triggersActionsUI.home.rulesTabTitle": "规则",
"xpack.triggersActionsUI.home.sectionDescription": "使规则检测条件,并使用连接器采取操作。",

View file

@ -31,11 +31,17 @@ import {
AlertsTableConfigurationRegistryContract,
RuleTypeRegistryContract,
} from '../types';
import { Section, routeToRuleDetails, legacyRouteToRuleDetails } from './constants';
import {
Section,
routeToRuleDetails,
legacyRouteToRuleDetails,
routeToConnectors,
} from './constants';
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';
const TriggersActionsUIHome = lazy(() => import('./home'));
const RuleDetailsRoute = lazy(
@ -73,7 +79,7 @@ export const renderApp = (deps: TriggersAndActionsUiServices) => {
export const App = ({ deps }: { deps: TriggersAndActionsUiServices }) => {
const { dataViews, uiSettings, theme$ } = deps;
const sections: Section[] = ['rules', 'connectors', 'logs', 'alerts'];
const sections: Section[] = ['rules', 'logs', 'alerts'];
const isDarkMode = useObservable<boolean>(uiSettings.get$('theme:darkMode'));
const sectionsRegex = sections.join('|');
@ -96,6 +102,7 @@ export const App = ({ deps }: { deps: TriggersAndActionsUiServices }) => {
export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) => {
const {
actions: { validateEmailAddresses },
application: { navigateToApp },
} = useKibana().services;
return (
@ -114,6 +121,15 @@ export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) =
path={legacyRouteToRuleDetails}
render={({ match }) => <Redirect to={`/rule/${match.params.alertId}`} />}
/>
<Route
exact
path={routeToConnectors}
render={() => {
navigateToApp(`management/insightsAndAlerting/${CONNECTORS_PLUGIN_ID}`);
return null;
}}
/>
<Redirect from={'/'} to="rules" />
<Redirect from={'/alerts'} to="rules" />
</Switch>

View file

@ -7,11 +7,25 @@
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import { EuiButton, EuiEmptyPrompt, EuiIcon, EuiSpacer, EuiTitle } from '@elastic/eui';
import {
EuiButton,
EuiButtonEmpty,
EuiPageTemplate,
EuiIcon,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { DocLinksStart } from '@kbn/core-doc-links-browser';
import './empty_connectors_prompt.scss';
export const EmptyConnectorsPrompt = ({ onCTAClicked }: { onCTAClicked: () => void }) => (
<EuiEmptyPrompt
export const EmptyConnectorsPrompt = ({
onCTAClicked,
docLinks,
}: {
onCTAClicked: () => void;
docLinks: DocLinksStart;
}) => (
<EuiPageTemplate.EmptyPrompt
data-test-subj="createFirstConnectorEmptyPrompt"
title={
<>
@ -33,24 +47,39 @@ export const EmptyConnectorsPrompt = ({ onCTAClicked }: { onCTAClicked: () => vo
<p>
<FormattedMessage
id="xpack.triggersActionsUI.components.emptyConnectorsPrompt.addConnectorEmptyBody"
defaultMessage="Configure email, Slack, Elasticsearch, and third-party services that Kibana runs."
defaultMessage="Configure various third-party services to Kibana."
/>
</p>
}
actions={
<EuiButton
data-test-subj="createFirstActionButton"
key="create-action"
fill
iconType="plusInCircle"
iconSide="left"
onClick={onCTAClicked}
>
<FormattedMessage
id="xpack.triggersActionsUI.components.emptyConnectorsPrompt.addConnectorButtonLabel"
defaultMessage="Create connector"
/>
</EuiButton>
<>
<EuiButton
data-test-subj="createFirstActionButton"
key="create-action"
fill
iconType="plusInCircle"
iconSide="left"
onClick={onCTAClicked}
>
<FormattedMessage
id="xpack.triggersActionsUI.components.emptyConnectorsPrompt.addConnectorButtonLabel"
defaultMessage="Create connector"
/>
</EuiButton>
<br />
<EuiButtonEmpty
data-test-subj="documentationButton"
key="documentation-button"
target="_blank"
href={docLinks.links.alerting.connectors}
iconType="help"
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.actionsConnectorsList.documentationButtonLabel"
defaultMessage="Documentation"
/>
</EuiButtonEmpty>
</>
}
/>
);

View file

@ -0,0 +1,104 @@
/*
* 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, { lazy } from 'react';
import { Switch, Route, Router } from 'react-router-dom';
import { ChromeBreadcrumb, CoreStart, CoreTheme, ScopedHistory } from '@kbn/core/public';
import { render, unmountComponentAtNode } from 'react-dom';
import { I18nProvider } from '@kbn/i18n-react';
import useObservable from 'react-use/lib/useObservable';
import { Observable } from 'rxjs';
import { KibanaFeature } from '@kbn/features-plugin/common';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
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 { Storage } from '@kbn/kibana-utils-plugin/public';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public';
import { suspendedComponentWithProps } from './lib/suspended_component_with_props';
import {
ActionTypeRegistryContract,
AlertsTableConfigurationRegistryContract,
RuleTypeRegistryContract,
} from '../types';
import { setDataViewsService } from '../common/lib/data_apis';
import { KibanaContextProvider, useKibana } from '../common/lib/kibana';
import { ConnectorProvider } from './context/connector_context';
const ActionsConnectorsList = lazy(
() => import('./sections/actions_connectors_list/components/actions_connectors_list')
);
export interface TriggersAndActionsUiServices extends CoreStart {
actions: ActionsPublicPluginSetup;
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
dataViewEditor: DataViewEditorStart;
charts: ChartsPluginStart;
alerting?: AlertingStart;
spaces?: SpacesPluginStart;
storage?: Storage;
isCloud: boolean;
setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void;
actionTypeRegistry: ActionTypeRegistryContract;
ruleTypeRegistry: RuleTypeRegistryContract;
alertsTableConfigurationRegistry: AlertsTableConfigurationRegistryContract;
history: ScopedHistory;
kibanaFeatures: KibanaFeature[];
element: HTMLElement;
theme$: Observable<CoreTheme>;
unifiedSearch: UnifiedSearchPublicPluginStart;
}
export const renderApp = (deps: TriggersAndActionsUiServices) => {
const { element } = deps;
render(<App deps={deps} />, element);
return () => {
unmountComponentAtNode(element);
};
};
export const App = ({ deps }: { deps: TriggersAndActionsUiServices }) => {
const { dataViews, uiSettings, theme$ } = deps;
const isDarkMode = useObservable<boolean>(uiSettings.get$('theme:darkMode'));
setDataViewsService(dataViews);
return (
<I18nProvider>
<EuiThemeProvider darkMode={isDarkMode}>
<KibanaThemeProvider theme$={theme$}>
<KibanaContextProvider services={{ ...deps }}>
<Router history={deps.history}>
<AppWithoutRouter />
</Router>
</KibanaContextProvider>
</KibanaThemeProvider>
</EuiThemeProvider>
</I18nProvider>
);
};
export const AppWithoutRouter = () => {
const {
actions: { validateEmailAddresses },
} = useKibana().services;
return (
<ConnectorProvider value={{ services: { validateEmailAddresses } }}>
<Switch>
<Route path={'/'} component={suspendedComponentWithProps(ActionsConnectorsList, 'xl')} />
</Switch>
</ConnectorProvider>
);
};

View file

@ -9,7 +9,7 @@ export const PLUGIN = {
ID: 'triggersActionsUi',
getI18nName: (i18n: any): string => {
return i18n.translate('xpack.triggersActionsUI.appName', {
defaultMessage: 'Rules and Connectors',
defaultMessage: 'Rules',
});
},
};

View file

@ -69,8 +69,8 @@ describe('home', () => {
let home = mountWithIntl(<TriggersActionsUIHome {...props} />);
// Just rules/connectors
expect(home.find('.euiTab__content').length).toBe(3);
// Just rules/logs
expect(home.find('.euiTab__content').length).toBe(2);
(getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'internalAlertsTable') {
@ -81,6 +81,6 @@ describe('home', () => {
home = mountWithIntl(<TriggersActionsUIHome {...props} />);
// alerts now too!
expect(home.find('.euiTab__content').length).toBe(4);
expect(home.find('.euiTab__content').length).toBe(3);
});
});

View file

@ -11,25 +11,15 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer, EuiButtonEmpty, EuiPageHeader } from '@elastic/eui';
import { getIsExperimentalFeatureEnabled } from '../common/get_experimental_features';
import {
Section,
routeToConnectors,
routeToRules,
routeToInternalAlerts,
routeToLogs,
} from './constants';
import { Section, routeToRules, routeToInternalAlerts, routeToLogs } from './constants';
import { getAlertingSectionBreadcrumb } from './lib/breadcrumb';
import { getCurrentDocTitle } from './lib/doc_title';
import { hasShowActionsCapability } from './lib/capabilities';
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';
const ActionsConnectorsList = lazy(
() => import('./sections/actions_connectors_list/components/actions_connectors_list')
);
const RulesList = lazy(() => import('./sections/rules_list/components/rules_list'));
const LogsList = lazy(() => import('./sections/logs_list/components/logs_list'));
const AlertsPage = lazy(() => import('./sections/alerts_table/alerts_page'));
@ -44,16 +34,9 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
},
history,
}) => {
const {
chrome,
application: { capabilities },
setBreadcrumbs,
docLinks,
} = useKibana().services;
const { chrome, setBreadcrumbs, docLinks } = useKibana().services;
const isInternalAlertsTableEnabled = getIsExperimentalFeatureEnabled('internalAlertsTable');
const canShowActions = hasShowActionsCapability(capabilities);
const tabs: Array<{
id: Section;
name: React.ReactNode;
@ -66,18 +49,6 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
),
});
if (canShowActions) {
tabs.push({
id: 'connectors',
name: (
<FormattedMessage
id="xpack.triggersActionsUI.home.connectorsTabTitle"
defaultMessage="Connectors"
/>
),
});
}
tabs.push({
id: 'logs',
name: <FormattedMessage id="xpack.triggersActionsUI.home.logsTabTitle" defaultMessage="Logs" />,
@ -111,10 +82,7 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
bottomBorder
pageTitle={
<span data-test-subj="appTitle">
<FormattedMessage
id="xpack.triggersActionsUI.home.appTitle"
defaultMessage="Rules and Connectors"
/>
<FormattedMessage id="xpack.triggersActionsUI.home.appTitle" defaultMessage="Rules" />
</span>
}
rightSideItems={[
@ -133,7 +101,7 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
description={
<FormattedMessage
id="xpack.triggersActionsUI.home.sectionDescription"
defaultMessage="Detect conditions using rules, and take actions using connectors."
defaultMessage="Detect conditions using rules."
/>
}
tabs={tabs.map((tab) => ({
@ -155,13 +123,6 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
path={routeToLogs}
component={suspendedComponentWithProps(LogsList, 'xl')}
/>
{canShowActions && (
<Route
exact
path={routeToConnectors}
component={suspendedComponentWithProps(ActionsConnectorsList, 'xl')}
/>
)}
<Route
exact
path={routeToRules}

View file

@ -25,7 +25,7 @@ describe('getAlertingSectionBreadcrumb', () => {
});
expect(getAlertingSectionBreadcrumb('home', true)).toMatchObject({
text: i18n.translate('xpack.triggersActionsUI.home.breadcrumbTitle', {
defaultMessage: 'Rules and Connectors',
defaultMessage: 'Rules',
}),
href: `${routeToHome}`,
});
@ -44,7 +44,7 @@ describe('getAlertingSectionBreadcrumb', () => {
});
expect(getAlertingSectionBreadcrumb('home', false)).toMatchObject({
text: i18n.translate('xpack.triggersActionsUI.home.breadcrumbTitle', {
defaultMessage: 'Rules and Connectors',
defaultMessage: 'Rules',
}),
});
});

View file

@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import { routeToHome, routeToConnectors, routeToRules } from '../constants';
import { routeToHome, routeToConnectors, routeToRules, routeToLogs } from '../constants';
export const getAlertingSectionBreadcrumb = (
type: string,
@ -14,6 +14,17 @@ export const getAlertingSectionBreadcrumb = (
): { text: string; href?: string } => {
// Home and sections
switch (type) {
case 'logs':
return {
text: i18n.translate('xpack.triggersActionsUI.logs.breadcrumbTitle', {
defaultMessage: 'Logs',
}),
...(returnHref
? {
href: `${routeToLogs}`,
}
: {}),
};
case 'connectors':
return {
text: i18n.translate('xpack.triggersActionsUI.connectors.breadcrumbTitle', {
@ -39,7 +50,7 @@ export const getAlertingSectionBreadcrumb = (
default:
return {
text: i18n.translate('xpack.triggersActionsUI.home.breadcrumbTitle', {
defaultMessage: 'Rules and Connectors',
defaultMessage: 'Rules',
}),
...(returnHref
? {

View file

@ -9,7 +9,7 @@ import { getCurrentDocTitle } from './doc_title';
describe('getCurrentDocTitle', () => {
test('if change calls return the proper doc title ', async () => {
expect(getCurrentDocTitle('home') === 'Rules and Connectors').toBeTruthy();
expect(getCurrentDocTitle('home') === 'Rules').toBeTruthy();
expect(getCurrentDocTitle('connectors') === 'Connectors').toBeTruthy();
expect(getCurrentDocTitle('rules') === 'Rules').toBeTruthy();
});

View file

@ -11,6 +11,11 @@ export const getCurrentDocTitle = (page: string): string => {
let updatedTitle: string;
switch (page) {
case 'logs':
updatedTitle = i18n.translate('xpack.triggersActionsUI.logs.breadcrumbTitle', {
defaultMessage: 'Logs',
});
break;
case 'connectors':
updatedTitle = i18n.translate('xpack.triggersActionsUI.connectors.breadcrumbTitle', {
defaultMessage: 'Connectors',
@ -23,7 +28,7 @@ export const getCurrentDocTitle = (page: string): string => {
break;
default:
updatedTitle = i18n.translate('xpack.triggersActionsUI.home.breadcrumbTitle', {
defaultMessage: 'Rules and Connectors',
defaultMessage: 'Rules',
});
}
return updatedTitle;

View file

@ -22,6 +22,7 @@ import {
Criteria,
EuiButtonEmpty,
EuiBadge,
EuiPageTemplate,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { omit } from 'lodash';
@ -52,6 +53,8 @@ import {
} from '../../../../common/connectors_selection';
import { CreateConnectorFlyout } from '../../action_connector_form/create_connector_flyout';
import { EditConnectorFlyout } from '../../action_connector_form/edit_connector_flyout';
import { getAlertingSectionBreadcrumb } from '../../../lib/breadcrumb';
import { getCurrentDocTitle } from '../../../lib/doc_title';
const ConnectorIconTipWithSpacing = withTheme(({ theme }: { theme: EuiTheme }) => {
return (
@ -83,6 +86,9 @@ const ActionsConnectorsList: React.FunctionComponent = () => {
notifications: { toasts },
application: { capabilities },
actionTypeRegistry,
setBreadcrumbs,
chrome,
docLinks,
} = useKibana().services;
const canDelete = hasDeleteActionsCapability(capabilities);
const canExecute = hasExecuteActionsCapability(capabilities);
@ -107,6 +113,12 @@ const ActionsConnectorsList: React.FunctionComponent = () => {
}, []);
const [showWarningText, setShowWarningText] = useState<boolean>(false);
// Set breadcrumb and page title
useEffect(() => {
setBreadcrumbs([getAlertingSectionBreadcrumb('connectors')]);
chrome.docTitle.change(getCurrentDocTitle('connectors'));
}, [chrome, setBreadcrumbs]);
useEffect(() => {
(async () => {
try {
@ -428,110 +440,144 @@ const ActionsConnectorsList: React.FunctionComponent = () => {
/>
</EuiButton>,
],
toolsRight: canSave
? [
<EuiButton
data-test-subj="createActionButton"
key="create-action"
fill
onClick={() => setAddFlyoutVisibility(true)}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.actionsConnectorsList.addActionButtonLabel"
defaultMessage="Create connector"
/>
</EuiButton>,
]
: [],
}}
/>
);
return (
<section data-test-subj="actionsList">
<DeleteModalConfirmation
data-test-subj="deleteConnectorsConfirmation"
onDeleted={(deleted: string[]) => {
if (selectedItems.length === 0 || selectedItems.length === deleted.length) {
const updatedActions = actions.filter(
(action) => action.id && !connectorsToDelete.includes(action.id)
);
setActions(updatedActions);
setSelectedItems([]);
}
setConnectorsToDelete([]);
}}
onErrors={async () => {
// Refresh the actions from the server, some actions may have beend deleted
await loadActions();
setConnectorsToDelete([]);
}}
onCancel={async () => {
setConnectorsToDelete([]);
}}
apiDeleteCall={deleteActions}
idsToDelete={connectorsToDelete}
singleTitle={i18n.translate(
'xpack.triggersActionsUI.sections.actionsConnectorsList.singleTitle',
{ defaultMessage: 'connector' }
)}
multipleTitle={i18n.translate(
'xpack.triggersActionsUI.sections.actionsConnectorsList.multipleTitle',
{ defaultMessage: 'connectors' }
)}
showWarningText={showWarningText}
warningText={i18n.translate(
'xpack.triggersActionsUI.sections.actionsConnectorsList.warningText',
{
defaultMessage:
'{connectors, plural, one {This connector is} other {Some connectors are}} currently in use.',
values: {
connectors: connectorsToDelete.length,
},
}
)}
setIsLoadingState={(isLoading: boolean) => setIsLoadingActionTypes(isLoading)}
/>
<>
{actionConnectorTableItems.length !== 0 && (
<EuiPageTemplate.Header
paddingSize="none"
pageTitle={i18n.translate('xpack.triggersActionsUI.connectors.home.appTitle', {
defaultMessage: 'Connectors',
})}
description={i18n.translate('xpack.triggersActionsUI.connectors.home.description', {
defaultMessage: 'Connect third-party software with your alerting data.',
})}
rightSideItems={(canSave
? [
<EuiButton
data-test-subj="createActionButton"
key="create-action"
fill
onClick={() => setAddFlyoutVisibility(true)}
iconType="plusInCircle"
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.actionsConnectorsList.addActionButtonLabel"
defaultMessage="Create connector"
/>
</EuiButton>,
]
: []
).concat([
<EuiButtonEmpty
data-test-subj="documentationButton"
key="documentation-button"
href={docLinks.links.alerting.connectors}
iconType="help"
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.actionsConnectorsList.documentationButtonLabel"
defaultMessage="Documentation"
/>
</EuiButtonEmpty>,
])}
/>
)}
<EuiPageTemplate.Section
paddingSize="none"
data-test-subj="actionsList"
alignment={actionConnectorTableItems.length === 0 ? 'center' : 'top'}
>
<DeleteModalConfirmation
data-test-subj="deleteConnectorsConfirmation"
onDeleted={(deleted: string[]) => {
if (selectedItems.length === 0 || selectedItems.length === deleted.length) {
const updatedActions = actions.filter(
(action) => action.id && !connectorsToDelete.includes(action.id)
);
setActions(updatedActions);
setSelectedItems([]);
}
setConnectorsToDelete([]);
}}
onErrors={async () => {
// Refresh the actions from the server, some actions may have beend deleted
await loadActions();
setConnectorsToDelete([]);
}}
onCancel={async () => {
setConnectorsToDelete([]);
}}
apiDeleteCall={deleteActions}
idsToDelete={connectorsToDelete}
singleTitle={i18n.translate(
'xpack.triggersActionsUI.sections.actionsConnectorsList.singleTitle',
{ defaultMessage: 'connector' }
)}
multipleTitle={i18n.translate(
'xpack.triggersActionsUI.sections.actionsConnectorsList.multipleTitle',
{ defaultMessage: 'connectors' }
)}
showWarningText={showWarningText}
warningText={i18n.translate(
'xpack.triggersActionsUI.sections.actionsConnectorsList.warningText',
{
defaultMessage:
'{connectors, plural, one {This connector is} other {Some connectors are}} currently in use.',
values: {
connectors: connectorsToDelete.length,
},
}
)}
setIsLoadingState={(isLoading: boolean) => setIsLoadingActionTypes(isLoading)}
/>
<EuiSpacer size="m" />
{/* Render the view based on if there's data or if they can save */}
{(isLoadingActions || isLoadingActionTypes) && <CenterJustifiedSpinner />}
{actionConnectorTableItems.length !== 0 && table}
{actionConnectorTableItems.length === 0 &&
canSave &&
!isLoadingActions &&
!isLoadingActionTypes && (
<EmptyConnectorsPrompt onCTAClicked={() => setAddFlyoutVisibility(true)} />
)}
{actionConnectorTableItems.length === 0 && !canSave && <NoPermissionPrompt />}
{addFlyoutVisible ? (
<CreateConnectorFlyout
onClose={() => {
setAddFlyoutVisibility(false);
}}
onTestConnector={(connector) => editItem(connector, EditConnectorTabs.Test)}
onConnectorCreated={loadActions}
actionTypeRegistry={actionTypeRegistry}
/>
) : null}
{editConnectorProps.initialConnector ? (
<EditConnectorFlyout
key={`${editConnectorProps.initialConnector.id}${
editConnectorProps.tab ? `:${editConnectorProps.tab}` : ``
}`}
connector={editConnectorProps.initialConnector}
tab={editConnectorProps.tab}
onClose={() => {
setEditConnectorProps(omit(editConnectorProps, 'initialConnector'));
}}
onConnectorUpdated={(connector) => {
setEditConnectorProps({ ...editConnectorProps, initialConnector: connector });
loadActions();
}}
actionTypeRegistry={actionTypeRegistry}
/>
) : null}
</section>
<EuiSpacer size="m" />
{/* Render the view based on if there's data or if they can save */}
{(isLoadingActions || isLoadingActionTypes) && <CenterJustifiedSpinner />}
{actionConnectorTableItems.length !== 0 && table}
{actionConnectorTableItems.length === 0 &&
canSave &&
!isLoadingActions &&
!isLoadingActionTypes && (
<EmptyConnectorsPrompt
onCTAClicked={() => setAddFlyoutVisibility(true)}
docLinks={docLinks}
/>
)}
{actionConnectorTableItems.length === 0 && !canSave && <NoPermissionPrompt />}
{addFlyoutVisible ? (
<CreateConnectorFlyout
onClose={() => {
setAddFlyoutVisibility(false);
}}
onTestConnector={(connector) => editItem(connector, EditConnectorTabs.Test)}
onConnectorCreated={loadActions}
actionTypeRegistry={actionTypeRegistry}
/>
) : null}
{editConnectorProps.initialConnector ? (
<EditConnectorFlyout
key={`${editConnectorProps.initialConnector.id}${
editConnectorProps.tab ? `:${editConnectorProps.tab}` : ``
}`}
connector={editConnectorProps.initialConnector}
tab={editConnectorProps.tab}
onClose={() => {
setEditConnectorProps(omit(editConnectorProps, 'initialConnector'));
}}
onConnectorUpdated={(connector) => {
setEditConnectorProps({ ...editConnectorProps, initialConnector: connector });
loadActions();
}}
actionTypeRegistry={actionTypeRegistry}
/>
) : null}
</EuiPageTemplate.Section>
</>
);
};

View file

@ -12,3 +12,4 @@ export { builtInGroupByTypes } from './group_by_types';
export const VIEW_LICENSE_OPTIONS_LINK = 'https://www.elastic.co/subscriptions';
export const PLUGIN_ID = 'triggersActions';
export const CONNECTORS_PLUGIN_ID = 'triggersActionsConnectors';

View file

@ -69,7 +69,7 @@ import type {
} from './types';
import { TriggersActionsUiConfigType } from '../common/types';
import { registerAlertsTableConfiguration } from './application/sections/alerts_table/alerts_page/register_alerts_table_configuration';
import { PLUGIN_ID } from './common/constants';
import { PLUGIN_ID, CONNECTORS_PLUGIN_ID } from './common/constants';
import type { AlertsTableStateProps } from './application/sections/alerts_table/alerts_table_state';
import { getAlertsTableStateLazy } from './common/get_alerts_table_state';
import { ActionAccordionFormProps } from './application/sections/action_connector_form/action_form';
@ -182,12 +182,24 @@ export class Plugin
ExperimentalFeaturesService.init({ experimentalFeatures: this.experimentalFeatures });
const featureTitle = i18n.translate('xpack.triggersActionsUI.managementSection.displayName', {
defaultMessage: 'Rules and Connectors',
defaultMessage: 'Rules',
});
const featureDescription = i18n.translate(
'xpack.triggersActionsUI.managementSection.displayDescription',
{
defaultMessage: 'Detect conditions using rules, and take actions using connectors.',
defaultMessage: 'Detect conditions using rules.',
}
);
const connectorsFeatureTitle = i18n.translate(
'xpack.triggersActionsUI.managementSection.connectors.displayName',
{
defaultMessage: 'Connectors',
}
);
const connectorsFeatureDescription = i18n.translate(
'xpack.triggersActionsUI.managementSection.connectors.displayDescription',
{
defaultMessage: 'Connect third-party software with your alerting data.',
}
);
@ -201,6 +213,15 @@ export class Plugin
showOnHomePage: false,
category: 'admin',
});
plugins.home.featureCatalogue.register({
id: CONNECTORS_PLUGIN_ID,
title: connectorsFeatureTitle,
description: connectorsFeatureDescription,
icon: 'watchesApp',
path: '/app/management/insightsAndAlerting/triggersActions',
showOnHomePage: false,
category: 'admin',
});
}
plugins.management.sections.section.insightsAndAlerting.registerApp({
@ -250,6 +271,53 @@ export class Plugin
},
});
plugins.management.sections.section.insightsAndAlerting.registerApp({
id: CONNECTORS_PLUGIN_ID,
title: connectorsFeatureTitle,
order: 2,
async mount(params: ManagementAppMountParams) {
const [coreStart, pluginsStart] = (await core.getStartServices()) as [
CoreStart,
PluginsStart,
unknown
];
const { renderApp } = await import('./application/connectors_app');
// The `/api/features` endpoint requires the "Global All" Kibana privilege. Users with a
// subset of this privilege are not authorized to access this endpoint and will receive a 404
// error that causes the Alerting view to fail to load.
let kibanaFeatures: KibanaFeature[];
try {
kibanaFeatures = await pluginsStart.features.getFeatures();
} catch (err) {
kibanaFeatures = [];
}
return renderApp({
...coreStart,
actions: plugins.actions,
data: pluginsStart.data,
dataViews: pluginsStart.dataViews,
dataViewEditor: pluginsStart.dataViewEditor,
charts: pluginsStart.charts,
alerting: pluginsStart.alerting,
spaces: pluginsStart.spaces,
unifiedSearch: pluginsStart.unifiedSearch,
isCloud: Boolean(plugins.cloud?.isCloudEnabled),
element: params.element,
theme$: params.theme$,
storage: new Storage(window.localStorage),
setBreadcrumbs: params.setBreadcrumbs,
history: params.history,
actionTypeRegistry,
ruleTypeRegistry,
alertsTableConfigurationRegistry,
kibanaFeatures,
});
},
});
if (this.experimentalFeatures.internalAlertsTable) {
registerAlertsTableConfiguration({
alertsTableConfigurationRegistry: this.alertsTableConfigurationRegistry,

View file

@ -41,7 +41,7 @@ export class WatcherUIPlugin implements Plugin<void, void, Dependencies, any> {
const watcherESApp = esSection.registerApp({
id: 'watcher',
title: pluginName,
order: 3,
order: 5,
mount: async ({ element, setBreadcrumbs, history, theme$ }) => {
const [coreStart] = await getStartServices();
const {

View file

@ -64,7 +64,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
expect(sections).to.have.length(2);
expect(sections[0]).to.eql({
sectionId: 'insightsAndAlerting',
sectionLinks: ['triggersActions', 'cases', 'jobsListLink'],
sectionLinks: ['triggersActions', 'cases', 'triggersActionsConnectors', 'jobsListLink'],
});
expect(sections[1]).to.eql({
sectionId: 'kibana',

View file

@ -122,7 +122,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
afterEach(async () => {
// Reset the Rules tab without reloading the entire page
// This is safer than trying to close the alert flyout, which may or may not be open at the end of a test
await testSubjects.click('connectorsTab');
await testSubjects.click('logsTab');
await testSubjects.click('rulesTab');
});

View file

@ -18,6 +18,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const retry = getService('retry');
const supertest = getService('supertest');
const objectRemover = new ObjectRemover(supertest);
const browser = getService('browser');
describe('Connectors', function () {
before(async () => {
@ -26,8 +27,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
.set('kbn-xsrf', 'foo')
.send(getTestActionData())
.expect(200);
await pageObjects.common.navigateToApp('triggersActions');
await testSubjects.click('connectorsTab');
await pageObjects.common.navigateToApp('triggersActionsConnectors');
objectRemover.add(createdAction.id, 'action', 'actions');
});
@ -75,6 +75,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const updatedConnectorName = `${connectorName}updated`;
const createdAction = await createConnector(connectorName);
objectRemover.add(createdAction.id, 'action', 'actions');
await browser.refresh();
await pageObjects.triggersActionsUI.searchConnectors(connectorName);
@ -112,6 +113,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const indexName = generateUniqueKey();
const createdAction = await createIndexConnector(connectorName, indexName);
objectRemover.add(createdAction.id, 'action', 'actions');
await browser.refresh();
await pageObjects.triggersActionsUI.searchConnectors(connectorName);
@ -141,6 +143,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const indexName = generateUniqueKey();
const createdAction = await createIndexConnector(connectorName, indexName);
objectRemover.add(createdAction.id, 'action', 'actions');
await browser.refresh();
await pageObjects.triggersActionsUI.searchConnectors(connectorName);
@ -168,6 +171,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const connectorName = generateUniqueKey();
const createdAction = await createConnector(connectorName);
objectRemover.add(createdAction.id, 'action', 'actions');
await browser.refresh();
await pageObjects.triggersActionsUI.searchConnectors(connectorName);
const searchResultsBeforeEdit = await pageObjects.triggersActionsUI.getConnectorsList();
@ -195,6 +200,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await createConnector(connectorName);
const createdAction = await createConnector(generateUniqueKey());
objectRemover.add(createdAction.id, 'action', 'actions');
await browser.refresh();
await pageObjects.triggersActionsUI.searchConnectors(connectorName);
@ -220,6 +226,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await createConnector(connectorName);
const createdAction = await createConnector(generateUniqueKey());
objectRemover.add(createdAction.id, 'action', 'actions');
await browser.refresh();
await pageObjects.triggersActionsUI.searchConnectors(connectorName);
@ -285,7 +292,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
connector_type_id: '.slack',
})
.expect(200);
await testSubjects.click('connectorsTab');
return createdAction;
}
@ -303,7 +309,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
secrets: {},
})
.expect(200);
await testSubjects.click('connectorsTab');
return createdAction;
}

View file

@ -437,7 +437,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.existOrFail('rulesList');
// delete connector
await pageObjects.triggersActionsUI.changeTabs('connectorsTab');
await pageObjects.common.navigateToApp('triggersActionsConnectors');
await pageObjects.triggersActionsUI.searchConnectors(connector.name);
await testSubjects.click('deleteConnector');
await testSubjects.existOrFail('deleteIdsConfirmation');
@ -447,8 +447,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const toastTitle = await pageObjects.common.closeToast();
expect(toastTitle).to.eql('Deleted 1 connector');
// Wait to ensure the table is finished loading
await pageObjects.triggersActionsUI.tableFinishedLoading();
// click on first alert
await pageObjects.triggersActionsUI.changeTabs('rulesTab');
await pageObjects.common.navigateToApp('triggersActions');
await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(rule.name);
const editButton = await testSubjects.find('openEditRuleFlyoutButton');
@ -501,7 +504,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.existOrFail('rulesList');
// delete connector
await pageObjects.triggersActionsUI.changeTabs('connectorsTab');
await pageObjects.common.navigateToApp('triggersActionsConnectors');
await pageObjects.triggersActionsUI.searchConnectors(connector.name);
await testSubjects.click('deleteConnector');
await testSubjects.existOrFail('deleteIdsConfirmation');
@ -511,8 +514,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const toastTitle = await pageObjects.common.closeToast();
expect(toastTitle).to.eql('Deleted 1 connector');
// Wait to ensure the table is finished loading
await pageObjects.triggersActionsUI.tableFinishedLoading();
// click on first rule
await pageObjects.triggersActionsUI.changeTabs('rulesTab');
await pageObjects.common.navigateToApp('triggersActions');
await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name);
const editButton = await testSubjects.find('openEditRuleFlyoutButton');
@ -553,7 +559,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
// verify content
await testSubjects.existOrFail('rulesList');
await pageObjects.triggersActionsUI.changeTabs('connectorsTab');
await pageObjects.common.navigateToApp('triggersActionsConnectors');
await pageObjects.triggersActionsUI.searchConnectors('new connector');
await testSubjects.click('deleteConnector');
await testSubjects.existOrFail('deleteIdsConfirmation');

View file

@ -31,7 +31,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('Loads the Alerts page', async () => {
await pageObjects.common.navigateToApp('triggersActions');
const headingText = await pageObjects.triggersActionsUI.getSectionHeadingText();
expect(headingText).to.be('Rules and Connectors');
expect(headingText).to.be('Rules');
});
});
@ -45,8 +45,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('Loads the Alerts page but with error', async () => {
await pageObjects.common.navigateToApp('triggersActions');
const headingText = await pageObjects.triggersActionsUI.getRulesListTitle();
expect(headingText).to.be('No permissions to create rules');
const exists = await testSubjects.exists('noPermissionPrompt');
expect(exists).to.be(true);
});
});
@ -60,26 +60,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
it('Loads the Alerts page', async () => {
await log.debug('Checking for section heading to say Rules and Connectors.');
await log.debug('Checking for section heading to say Rules.');
const headingText = await pageObjects.triggersActionsUI.getSectionHeadingText();
expect(headingText).to.be('Rules and Connectors');
});
describe('Connectors tab', () => {
it('renders the connectors tab', async () => {
// Navigate to the connectors tab
await pageObjects.triggersActionsUI.changeTabs('connectorsTab');
await pageObjects.header.waitUntilLoadingHasFinished();
// Verify url
const url = await browser.getCurrentUrl();
expect(url).to.contain(`/connectors`);
// Verify content
await testSubjects.existOrFail('actionsList');
});
expect(headingText).to.be('Rules');
});
describe('Alerts tab', () => {

View file

@ -61,6 +61,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
triggersActions: {
pathname: '/app/management/insightsAndAlerting/triggersActions',
},
triggersActionsConnectors: {
pathname: '/app/management/insightsAndAlerting/triggersActionsConnectors',
},
},
esTestCluster: {
...xpackFunctionalConfig.get('esTestCluster'),

View file

@ -61,6 +61,11 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext)
await this.clickCreateFirstConnectorButton();
}
},
async tableFinishedLoading() {
await find.byCssSelector(
'.euiBasicTable[data-test-subj="actionsTable"]:not(.euiBasicTable-loading)'
);
},
async searchConnectors(searchText: string) {
const searchBox = await find.byCssSelector('[data-test-subj="actionsList"] .euiFieldSearch');
await searchBox.click();