diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc
index 23d80f100b4b4..085fc3e4a4cbc 100644
--- a/docs/user/dashboard/dashboard.asciidoc
+++ b/docs/user/dashboard/dashboard.asciidoc
@@ -228,6 +228,20 @@ To view the data field summary information, navigate to the field, then click *i
[role="screenshot"]
image::images/lens_data_info.png[Data summary window]
+*Lens* shows a summary depending on the type of data: date fields show the time distribution, string fields show the top 10 values, and numeric fields show a detailed summary with the top 10 values and a value distribution.
+
+[role="screenshot"]
+image::images/lens_data_info_documents.png[Data summary analyzed documents]
+
+*Lens* uses a sample of 5,000 documents to perform the field analysis. The bottom line of the summary shows the percentage of sampled documents over all available documents.
+
+When *Lens* presents the top 10 values distribution, it also shows the percentage of "Other" values. For array value fields, the percentage distribution is considers each value in the array separate.
+
+[role="screenshot"]
+image::images/lens_data_info_other.png[Data summary window with Other]
+
+NOTE: the sum of all the entries and "Other" may be above 100% by a small amount, that is due to a rounding problem when presenting all values as integers.
+
[float]
[[change-the-visualization-type]]
==== Change the visualization type
diff --git a/docs/user/dashboard/images/lens_data_info_documents.png b/docs/user/dashboard/images/lens_data_info_documents.png
new file mode 100644
index 0000000000000..880ade8023ad3
Binary files /dev/null and b/docs/user/dashboard/images/lens_data_info_documents.png differ
diff --git a/docs/user/dashboard/images/lens_data_info_other.png b/docs/user/dashboard/images/lens_data_info_other.png
new file mode 100644
index 0000000000000..b7e6ce78e22d1
Binary files /dev/null and b/docs/user/dashboard/images/lens_data_info_other.png differ
diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js
index eb75358c7c77c..59a10379a9423 100644
--- a/test/api_integration/apis/saved_objects/find.js
+++ b/test/api_integration/apis/saved_objects/find.js
@@ -78,7 +78,8 @@ export default function ({ getService }) {
}));
});
- describe('page beyond total', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/85911
+ describe.skip('page beyond total', () => {
it('should return 200 with empty response', async () =>
await supertest
.get('/api/saved_objects/_find?type=visualization&page=100&per_page=100')
@@ -419,7 +420,7 @@ export default function ({ getService }) {
}));
});
- describe('without kibana index', () => {
+ describe.skip('without kibana index', () => {
before(
async () =>
// just in case the kibana server has recreated it
diff --git a/x-pack/examples/alerting_example/public/components/create_alert.tsx b/x-pack/examples/alerting_example/public/components/create_alert.tsx
index 58f9542709623..db7667411a27e 100644
--- a/x-pack/examples/alerting_example/public/components/create_alert.tsx
+++ b/x-pack/examples/alerting_example/public/components/create_alert.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useMemo, useState } from 'react';
+import React, { useMemo, useState, useCallback } from 'react';
import { EuiIcon, EuiFlexItem, EuiCard, EuiFlexGroup } from '@elastic/eui';
@@ -16,15 +16,18 @@ export const CreateAlert = ({
}: Pick) => {
const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false);
+ const onCloseAlertFlyout = useCallback(() => setAlertFlyoutVisibility(false), [
+ setAlertFlyoutVisibility,
+ ]);
+
const AddAlertFlyout = useMemo(
() =>
triggersActionsUi.getAddAlertFlyout({
consumer: ALERTING_EXAMPLE_APP_ID,
- addFlyoutVisible: alertFlyoutVisible,
- setAddFlyoutVisibility: setAlertFlyoutVisibility,
+ onClose: onCloseAlertFlyout,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
- [alertFlyoutVisible]
+ [onCloseAlertFlyout]
);
return (
@@ -37,7 +40,7 @@ export const CreateAlert = ({
onClick={() => setAlertFlyoutVisibility(true)}
/>
- {AddAlertFlyout}
+ {alertFlyoutVisible && AddAlertFlyout}
);
};
diff --git a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx
index aa1d21dd1d580..88a897d7baf50 100644
--- a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx
+++ b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useMemo } from 'react';
+import React, { useCallback, useMemo } from 'react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { AlertType } from '../../../../common/alert_types';
import { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public';
@@ -23,17 +23,21 @@ export function AlertingFlyout(props: Props) {
const {
services: { triggersActionsUi },
} = useKibana();
+
+ const onCloseAddFlyout = useCallback(() => setAddFlyoutVisibility(false), [
+ setAddFlyoutVisibility,
+ ]);
+
const addAlertFlyout = useMemo(
() =>
alertType &&
triggersActionsUi.getAddAlertFlyout({
consumer: 'apm',
- addFlyoutVisible,
- setAddFlyoutVisibility,
+ onClose: onCloseAddFlyout,
alertTypeId: alertType,
canChangeTrigger: false,
}),
- [addFlyoutVisible, alertType, setAddFlyoutVisibility, triggersActionsUi]
+ [alertType, onCloseAddFlyout, triggersActionsUi]
);
- return <>{addAlertFlyout}>;
+ return <>{addFlyoutVisible && addAlertFlyout}>;
}
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx
index 1662f44d1e421..e63d30022360e 100644
--- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx
@@ -66,9 +66,9 @@ export function ServiceOverviewThroughputChart({
type: 'linemark',
color: theme.eui.euiColorVis0,
title: i18n.translate(
- 'xpack.apm.serviceOverview.throughputChart.currentPeriodLabel',
+ 'xpack.apm.serviceOverview.throughputChart.traffic',
{
- defaultMessage: 'Current period',
+ defaultMessage: 'Traffic',
}
),
},
diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx
index 0917839ad631b..e620acd56aadd 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx
@@ -46,6 +46,7 @@ export function SparkPlot({
const defaultChartTheme = useChartTheme();
const sparkplotChartTheme = merge({}, defaultChartTheme, {
+ chartMargins: { left: 0, right: 0, top: 0, bottom: 0 },
lineSeriesStyle: {
point: { opacity: 0 },
},
diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx
index 4a388b13d7d22..4a0972b519ad5 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx
@@ -19,13 +19,6 @@ function yLabelFormat(y?: number | null) {
return asPercent(y || 0, 1);
}
-function yTickFormat(y?: number | null) {
- return i18n.translate('xpack.apm.chart.averagePercentLabel', {
- defaultMessage: '{y} (avg.)',
- values: { y: yLabelFormat(y) },
- });
-}
-
interface Props {
height?: number;
showAnnotations?: boolean;
@@ -84,13 +77,12 @@ export function TransactionErrorRateChart({
type: 'linemark',
color: theme.eui.euiColorVis7,
hideLegend: true,
- title: i18n.translate('xpack.apm.errorRate.currentPeriodLabel', {
- defaultMessage: 'Current period',
+ title: i18n.translate('xpack.apm.errorRate.chart.errorRate', {
+ defaultMessage: 'Error rate (avg.)',
}),
},
]}
yLabelFormat={yLabelFormat}
- yTickFormat={yTickFormat}
yDomain={{ min: 0, max: 1 }}
/>
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.tsx
index 7f4e6660e088f..4f730da8b974a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.tsx
@@ -11,7 +11,11 @@ import { Paging, ResultsPerPage } from '@elastic/react-search-ui';
import { PagingView, ResultsPerPageView } from './views';
export const Pagination: React.FC<{ 'aria-label': string }> = ({ 'aria-label': ariaLabel }) => (
-
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/search_box_view.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/search_box_view.tsx
index 002c9f84810ff..e8ccc10762dd7 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/search_box_view.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/search_box_view.tsx
@@ -23,7 +23,7 @@ export const SearchBoxView: React.FC = ({ onChange, value, inputProps })
onChange(event.target.value)}
- fullWidth={true}
+ fullWidth
{...inputProps}
/>
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.tsx
index db56dfcca286e..c84717545284e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.tsx
@@ -33,8 +33,9 @@ export const SortingView: React.FC = ({ onChange, options, value }) => {
const selectedValue = value && !valuesFromOptions.includes(value) ? undefined : value;
return (
-
+ <>
= ({ onChange, options, value }) => {
}
)}
/>
-
+ >
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx
index 23115fff88f51..2c403caf4dc3f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx
@@ -8,6 +8,14 @@ import React from 'react';
import { EuiSpacer } from '@elastic/eui';
+import {
+ PUBLIC_KEY_LABEL,
+ CONSUMER_KEY_LABEL,
+ BASE_URL_LABEL,
+ CLIENT_ID_LABEL,
+ CLIENT_SECRET_LABEL,
+} from '../../../constants';
+
import { ApiKey } from '../api_key';
import { CredentialItem } from '../credential_item';
@@ -35,13 +43,13 @@ export const SourceConfigFields: React.FC = ({
<>
{publicKey && (
<>
-
+
>
)}
{consumerKey && (
<>
-
+
>
)}
@@ -51,11 +59,11 @@ export const SourceConfigFields: React.FC = ({
return (
<>
{showApiKey && keyElement}
- {credentialItem('Client id', clientId)}
+ {credentialItem(CLIENT_ID_LABEL, clientId)}
- {credentialItem('Client secret', clientSecret)}
+ {credentialItem(CLIENT_SECRET_LABEL, clientSecret)}
- {credentialItem('Base URL', baseUrl)}
+ {credentialItem(BASE_URL_LABEL, baseUrl)}
>
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
index 327ee7b30582b..3e828d0fe80de 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
@@ -260,3 +260,52 @@ export const GITHUB_LINK_TITLE = i18n.translate(
);
export const CUSTOM_SERVICE_TYPE = 'custom';
+
+export const DOCUMENTATION_LINK_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.documentation',
+ {
+ defaultMessage: 'Documentation',
+ }
+);
+
+export const PUBLIC_KEY_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearc.publicKey.label',
+ {
+ defaultMessage: 'Public Key',
+ }
+);
+
+export const CONSUMER_KEY_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearc.consumerKey.label',
+ {
+ defaultMessage: 'Consumer Key',
+ }
+);
+
+export const BASE_URI_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearc.baseUri.label',
+ {
+ defaultMessage: 'Base URI',
+ }
+);
+
+export const BASE_URL_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearc.baseUrl.label',
+ {
+ defaultMessage: 'Base URL',
+ }
+);
+
+export const CLIENT_ID_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearc.clientId.label',
+ {
+ defaultMessage: 'Client id',
+ }
+);
+
+export const CLIENT_SECRET_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearc.clientSecret.label',
+ {
+ defaultMessage: 'Client secret',
+ }
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx
index 6914a26c9aeb4..d5524af1e163c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx
@@ -26,21 +26,22 @@ import { Loading } from '../../../../../../applications/shared/loading';
import { CUSTOM_SERVICE_TYPE } from '../../../../constants';
import { SourceDataItem } from '../../../../types';
+import {
+ ADD_SOURCE_NEW_SOURCE_DESCRIPTION,
+ ADD_SOURCE_ORG_SOURCE_DESCRIPTION,
+ ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION,
+ ADD_SOURCE_NO_SOURCES_TITLE,
+ ADD_SOURCE_ORG_SOURCES_TITLE,
+ ADD_SOURCE_PRIVATE_SOURCES_TITLE,
+ ADD_SOURCE_PLACEHOLDER,
+ ADD_SOURCE_EMPTY_TITLE,
+ ADD_SOURCE_EMPTY_BODY,
+} from './constants';
+
import { SourcesLogic } from '../../sources_logic';
import { AvailableSourcesList } from './available_sources_list';
import { ConfiguredSourcesList } from './configured_sources_list';
-const NEW_SOURCE_DESCRIPTION =
- 'When configuring and connecting a source, you are creating distinct entities with searchable content synchronized from the content platform itself. A source can be added using one of the available source connectors or via Custom API Sources, for additional flexibility. ';
-const ORG_SOURCE_DESCRIPTION =
- 'Shared content sources are available to your entire organization or can be assigned to specific user groups.';
-const PRIVATE_SOURCE_DESCRIPTION =
- 'Connect a new source to add its content and documents to your search experience.';
-const NO_SOURCES_TITLE = 'Configure and connect your first content source';
-const ORG_SOURCES_TITLE = 'Add a shared content source';
-const PRIVATE_SOURCES_TITLE = 'Add a new content source';
-const PLACEHOLDER = 'Filter sources...';
-
export const AddSourceList: React.FC = () => {
const { contentSources, dataLoading, availableSources, configuredSources } = useValues(
SourcesLogic
@@ -64,14 +65,16 @@ export const AddSourceList: React.FC = () => {
({ serviceType }) => serviceType !== CUSTOM_SERVICE_TYPE
);
- const BASE_DESCRIPTION = hasSources ? '' : NEW_SOURCE_DESCRIPTION;
+ const BASE_DESCRIPTION = hasSources ? '' : ADD_SOURCE_NEW_SOURCE_DESCRIPTION;
const PAGE_CONTEXT_DESCRIPTION = isOrganization
- ? ORG_SOURCE_DESCRIPTION
- : PRIVATE_SOURCE_DESCRIPTION;
+ ? ADD_SOURCE_ORG_SOURCE_DESCRIPTION
+ : ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION;
const PAGE_DESCRIPTION = BASE_DESCRIPTION + PAGE_CONTEXT_DESCRIPTION;
- const HAS_SOURCES_TITLE = isOrganization ? ORG_SOURCES_TITLE : PRIVATE_SOURCES_TITLE;
- const PAGE_TITLE = hasSources ? HAS_SOURCES_TITLE : NO_SOURCES_TITLE;
+ const HAS_SOURCES_TITLE = isOrganization
+ ? ADD_SOURCE_ORG_SOURCES_TITLE
+ : ADD_SOURCE_PRIVATE_SOURCES_TITLE;
+ const PAGE_TITLE = hasSources ? HAS_SOURCES_TITLE : ADD_SOURCE_NO_SOURCES_TITLE;
const handleFilterChange = (e: ChangeEvent) => setFilterValue(e.target.value);
@@ -106,7 +109,7 @@ export const AddSourceList: React.FC = () => {
value={filterValue}
onChange={handleFilterChange}
fullWidth={true}
- placeholder={PLACEHOLDER}
+ placeholder={ADD_SOURCE_PLACEHOLDER}
/>
@@ -128,13 +131,8 @@ export const AddSourceList: React.FC = () => {
No available sources}
- body={
-
- Sources will be available for search when an administrator adds them to this
- organization.
-
- }
+ title={{ADD_SOURCE_EMPTY_TITLE} }
+ body={{ADD_SOURCE_EMPTY_BODY}
}
/>
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx
index 0d4345c67cfb3..b9117f7da0fe5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx
@@ -5,7 +5,8 @@
*/
import React from 'react';
-import { Link } from 'react-router-dom';
+
+import { i18n } from '@kbn/i18n';
import {
EuiCard,
@@ -19,12 +20,20 @@ import {
import { useValues } from 'kea';
-import { LicensingLogic } from '../../../../../../applications/shared/licensing';
+import { LicensingLogic } from '../../../../../shared/licensing';
+import { EuiLinkTo } from '../../../../../shared/react_router_helpers';
import { SourceIcon } from '../../../../components/shared/source_icon';
import { SourceDataItem } from '../../../../types';
import { ADD_CUSTOM_PATH, getSourcesPath } from '../../../../routes';
+import {
+ AVAILABLE_SOURCE_EMPTY_STATE,
+ AVAILABLE_SOURCE_TITLE,
+ AVAILABLE_SOURCE_BODY,
+ AVAILABLE_SOURCE_CUSTOM_SOURCE_BUTTON,
+} from './constants';
+
interface AvailableSourcesListProps {
sources: SourceDataItem[];
}
@@ -54,13 +63,20 @@ export const AvailableSourcesList: React.FC = ({ sour
return (
{card}
);
}
- return {card};
+ return {card} ;
};
const visibleSources = (
@@ -73,19 +89,24 @@ export const AvailableSourcesList: React.FC = ({ sour
);
- const emptyState = No available sources matching your query.
;
+ const emptyState = (
+ {AVAILABLE_SOURCE_EMPTY_STATE}
+ );
return (
<>
- Available for configuration
+ {AVAILABLE_SOURCE_TITLE}
- Configure an available source or build your own with the{' '}
-
- Custom API Source
-
+ {AVAILABLE_SOURCE_BODY}
+
+ {AVAILABLE_SOURCE_CUSTOM_SOURCE_BUTTON}
+
.
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx
index 0409bbf578d5a..75d4f174c0aa8 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx
@@ -6,7 +6,8 @@
import React from 'react';
-import { Link } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButton,
@@ -26,6 +27,13 @@ import {
PRIVATE_SOURCES_DOCS_URL,
} from '../../../../routes';
+import { EuiLinkTo, EuiButtonTo } from '../../../../../shared/react_router_helpers';
+
+import {
+ CONFIG_COMPLETED_PRIVATE_SOURCES_DOCS_LINK,
+ CONFIG_COMPLETED_CONFIGURE_NEW_BUTTON,
+} from './constants';
+
interface ConfigCompletedProps {
header: React.ReactNode;
name: string;
@@ -58,28 +66,59 @@ export const ConfigCompleted: React.FC = ({
- {name} Configured
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configCompleted.heading',
+ {
+ defaultMessage: '{name} Configured',
+ values: { name },
+ }
+ )}
+
{!accountContextOnly ? (
- {name} can now be connected to Workplace Search
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configCompleted.orgCanConnect.message',
+ {
+ defaultMessage: '{name} can now be connected to Workplace Search',
+ values: { name },
+ }
+ )}
+
) : (
- Users can now link their {name} accounts from their personal dashboards.
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configCompleted.personalConnectLink.message',
+ {
+ defaultMessage:
+ 'Users can now link their {name} accounts from their personal dashboards.',
+ values: { name },
+ }
+ )}
+
{!privateSourcesEnabled && (
- Remember to{' '}
-
- enable private source connection
- {' '}
- in Security settings.
+
+ enable private source connection
+
+ ),
+ }}
+ />
)}
- Learn more about private content sources.
+ {CONFIG_COMPLETED_PRIVATE_SOURCES_DOCS_LINK}
@@ -93,16 +132,25 @@ export const ConfigCompleted: React.FC = ({
-
-
- Configure a new content source
-
-
+
+ {CONFIG_COMPLETED_CONFIGURE_NEW_BUTTON}
+
{!accountContextOnly && (
-
- Connect {name}
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configCompleted.connect.button',
+ {
+ defaultMessage: 'Connect {name}',
+ values: { name },
+ }
+ )}
)}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.tsx
index b666c859948d5..34cc902f8701a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.tsx
@@ -6,8 +6,12 @@
import React from 'react';
+import { i18n } from '@kbn/i18n';
+
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { DOCUMENTATION_LINK_TITLE } from '../../../../constants';
+
interface ConfigDocsLinksProps {
name: string;
documentationUrl: string;
@@ -24,13 +28,20 @@ export const ConfigDocsLinks: React.FC = ({
- Documentation
+ {DOCUMENTATION_LINK_TITLE}
{applicationPortalUrl && (
- {applicationLinkTitle || `${name} Application Portal`}
+ {applicationLinkTitle ||
+ i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configDocs.applicationPortal.button',
+ {
+ defaultMessage: '{name} Application Portal',
+ values: { name },
+ }
+ )}
)}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx
index 3e616a70031ac..ff15078361a95 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx
@@ -6,6 +6,9 @@
import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
import {
EuiBadge,
EuiButton,
@@ -17,6 +20,16 @@ import {
EuiTitle,
} from '@elastic/eui';
+import {
+ CONFIG_INTRO_ALT_TEXT,
+ CONFIG_INTRO_STEPS_TEXT,
+ CONFIG_INTRO_STEP1_HEADING,
+ CONFIG_INTRO_STEP1_TEXT,
+ CONFIG_INTRO_STEP2_HEADING,
+ CONFIG_INTRO_STEP2_TITLE,
+ CONFIG_INTRO_STEP2_TEXT,
+} from './constants';
+
import connectionIllustration from '../../../../assets/connection_illustration.svg';
interface ConfigurationIntroProps {
@@ -48,7 +61,7 @@ export const ConfigurationIntro: React.FC = ({
>
-
+
@@ -56,11 +69,19 @@ export const ConfigurationIntro: React.FC = ({
- How to add {name}
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configIntro.steps.title',
+ {
+ defaultMessage: 'How to add {name}',
+ values: { name },
+ }
+ )}
+
- Quick setup, then all of your documents will be searchable.
+ {CONFIG_INTRO_STEPS_TEXT}
@@ -69,21 +90,22 @@ export const ConfigurationIntro: React.FC = ({
- Step 1
+ {CONFIG_INTRO_STEP1_HEADING}
- Configure an OAuth application
- One-Time Action
+ One-Time Action,
+ }}
+ />
-
- Setup a secure OAuth application through the content source that you or your
- team will use to connect and synchronize content. You only have to do this
- once per content source.
-
+ {CONFIG_INTRO_STEP1_TEXT}
@@ -93,17 +115,14 @@ export const ConfigurationIntro: React.FC = ({
- Step 2
+ {CONFIG_INTRO_STEP2_HEADING}
- Connect the content source
-
- Use the new OAuth application to connect any number of instances of the
- content source to Workplace Search.
-
+ {CONFIG_INTRO_STEP2_TITLE}
+ {CONFIG_INTRO_STEP2_TEXT}
@@ -117,7 +136,13 @@ export const ConfigurationIntro: React.FC = ({
fill
onClick={advanceStep}
>
- Configure {name}
+ {i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configIntro.configure.button',
+ {
+ defaultMessage: 'Configure {name}',
+ values: { name },
+ }
+ )}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx
index 3788071979e67..1ff62ad5297d5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx
@@ -8,6 +8,8 @@ import React, { ChangeEvent, FormEvent } from 'react';
import { useActions, useValues } from 'kea';
+import { FormattedMessage } from '@kbn/i18n/react';
+
import {
EuiButton,
EuiFieldText,
@@ -20,6 +22,7 @@ import {
import { CUSTOM_SOURCE_DOCS_URL } from '../../../../routes';
import { SourceLogic } from '../../source_logic';
+import { CONFIG_CUSTOM_BUTTON } from './constants';
interface ConfigureCustomProps {
header: React.ReactNode;
@@ -51,10 +54,17 @@ export const ConfigureCustom: React.FC = ({
{helpText}
-
- Read the documentation
- {' '}
- to learn more about Custom API Sources.
+
+ Read the documentation
+
+ ),
+ }}
+ />
@@ -76,7 +86,7 @@ export const ConfigureCustom: React.FC = ({
isLoading={buttonLoading}
data-test-subj="CreateCustomButton"
>
- Create Custom API Source
+ {CONFIG_CUSTOM_BUTTON}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx
index 9c2084483c816..50955cbb81be1 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx
@@ -25,6 +25,8 @@ import { parseQueryParams } from '../../../../../../applications/shared/query_pa
import { Loading } from '../../../../../../applications/shared/loading';
import { SourceLogic } from '../../source_logic';
+import { CONFIG_OAUTH_LABEL, CONFIG_OAUTH_BUTTON } from './constants';
+
interface OauthQueryParams {
preContentSourceId: string;
}
@@ -78,7 +80,7 @@ export const ConfigureOauth: React.FC = ({ name, onFormCrea
responsive={false}
>
-
+
= ({ name, onFormCrea
- Complete connection
+ {CONFIG_OAUTH_BUTTON}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx
index fbd053f9b8374..a24951a8e54c6 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx
@@ -6,10 +6,7 @@
import React from 'react';
-import { Link } from 'react-router-dom';
-
import {
- EuiButtonEmpty,
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
@@ -21,10 +18,20 @@ import {
EuiToolTip,
} from '@elastic/eui';
+import { EuiButtonEmptyTo } from '../../../../../shared/react_router_helpers';
import { SourceIcon } from '../../../../components/shared/source_icon';
import { SourceDataItem } from '../../../../types';
import { getSourcesPath } from '../../../../routes';
+import {
+ CONFIGURED_SOURCES_LIST_UNCONNECTED_TOOLTIP,
+ CONFIGURED_SOURCES_LIST_ACCOUNT_ONLY_TOOLTIP,
+ CONFIGURED_SOURCES_CONNECT_BUTTON,
+ CONFIGURED_SOURCES_EMPTY_STATE,
+ CONFIGURED_SOURCES_TITLE,
+ CONFIGURED_SOURCES_EMPTY_BODY,
+} from './constants';
+
interface ConfiguredSourcesProps {
sources: SourceDataItem[];
isOrganization: boolean;
@@ -36,7 +43,7 @@ export const ConfiguredSourcesList: React.FC = ({
}) => {
const unConnectedTooltip = (
-
+
@@ -44,10 +51,7 @@ export const ConfiguredSourcesList: React.FC = ({
const accountOnlyTooltip = (
-
+
@@ -90,9 +94,9 @@ export const ConfiguredSourcesList: React.FC = ({
{(!isOrganization || (isOrganization && !accountContextOnly)) && (
-
- Connect
-
+
+ {CONFIGURED_SOURCES_CONNECT_BUTTON}
+
)}
@@ -103,15 +107,15 @@ export const ConfiguredSourcesList: React.FC = ({
);
- const emptyState = There are no configured sources matching your query.
;
+ const emptyState = {CONFIGURED_SOURCES_EMPTY_STATE}
;
return (
<>
- Configured content sources
+ {CONFIGURED_SOURCES_TITLE}
- Configured and ready for connection.
+ {CONFIGURED_SOURCES_EMPTY_BODY}
{sources.length > 0 ? visibleSources : emptyState}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx
index f9123ab4e1cca..c5b5644219b57 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx
@@ -7,6 +7,8 @@
import React, { useState, useEffect, FormEvent } from 'react';
import { useActions, useValues } from 'kea';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButton,
@@ -33,6 +35,17 @@ import { FeatureIds, Configuration, Features } from '../../../../types';
import { DOCUMENT_PERMISSIONS_DOCS_URL } from '../../../../routes';
import { SourceFeatures } from './source_features';
+import {
+ CONNECT_REMOTE,
+ CONNECT_PRIVATE,
+ CONNECT_WHICH_OPTION_LINK,
+ CONNECT_DOC_PERMISSIONS_LABEL,
+ CONNECT_DOC_PERMISSIONS_TITLE,
+ CONNECT_NEEDS_PERMISSIONS,
+ CONNECT_NOT_SYNCED_TITLE,
+ CONNECT_NOT_SYNCED_TEXT,
+} from './constants';
+
interface ConnectInstanceProps {
header: React.ReactNode;
configuration: Configuration;
@@ -145,8 +158,8 @@ export const ConnectInstance: React.FC = ({
return (
<>
- {isRemote && Remote }
- {isPrivate && Private }
+ {isRemote && {CONNECT_REMOTE} }
+ {isPrivate && {CONNECT_PRIVATE} }
>
@@ -164,7 +177,7 @@ export const ConnectInstance: React.FC = ({
const whichDocsLink = (
- Which option should I choose?
+ {CONNECT_WHICH_OPTION_LINK}
);
@@ -172,12 +185,12 @@ export const ConnectInstance: React.FC = ({
<>
- Document-level permissions
+ {CONNECT_DOC_PERMISSIONS_TITLE}
Enable document-level permission synchronization}
+ label={{CONNECT_DOC_PERMISSIONS_LABEL} }
name="index_permissions"
onChange={(e) => setSourceIndexPermissionsValue(e.target.checked)}
checked={indexPermissionsValue}
@@ -187,16 +200,22 @@ export const ConnectInstance: React.FC = ({
{!needsPermissions && (
- Document-level permissions are not yet available for this source.{' '}
-
- Learn more
-
+
+ Learn more
+
+ ),
+ }}
+ />
)}
{needsPermissions && indexPermissionsValue && (
- Document-level permission information will be synchronized. Additional configuration is
- required following the initial connection before documents are available for search.
+ {CONNECT_NEEDS_PERMISSIONS}
{whichDocsLink}
@@ -204,11 +223,10 @@ export const ConnectInstance: React.FC = ({
{!indexPermissionsValue && (
-
+
- All documents accessible to the connecting service user will be synchronized and made
- available to the organization’s users, or group’s users. Documents are immediately
- available for search. {needsPermissions && whichDocsLink}
+ {CONNECT_NOT_SYNCED_TEXT}
+ {needsPermissions && whichDocsLink}
)}
@@ -225,6 +243,10 @@ export const ConnectInstance: React.FC = ({
Connect {name}
+ {i18n.translate('xpack.enterpriseSearch.workplaceSearch.contentSource.connect.button', {
+ defaultMessage: 'Connect {name}',
+ values: { name },
+ })}
>
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts
new file mode 100644
index 0000000000000..09a9d22461e14
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts
@@ -0,0 +1,446 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const ADD_SOURCE_NEW_SOURCE_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.addSourceList.newSourceDescription',
+ {
+ defaultMessage:
+ 'When configuring and connecting a source, you are creating distinct entities with searchable content synchronized from the content platform itself. A source can be added using one of the available source connectors or via Custom API Sources, for additional flexibility. ',
+ }
+);
+
+export const ADD_SOURCE_ORG_SOURCE_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.addSourceList.orgSourceDescription',
+ {
+ defaultMessage:
+ 'Shared content sources are available to your entire organization or can be assigned to specific user groups.',
+ }
+);
+
+export const ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.addSourceList.privateSourceDescription',
+ {
+ defaultMessage:
+ 'Connect a new source to add its content and documents to your search experience.',
+ }
+);
+
+export const ADD_SOURCE_NO_SOURCES_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.addSourceList.noSourcesTitle',
+ {
+ defaultMessage: 'Configure and connect your first content source',
+ }
+);
+
+export const ADD_SOURCE_ORG_SOURCES_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.addSourceList.orgSourcesTitle',
+ {
+ defaultMessage: 'Add a shared content source',
+ }
+);
+
+export const ADD_SOURCE_PRIVATE_SOURCES_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.addSourceList.privateSourcesTitle',
+ {
+ defaultMessage: 'Add a new content source',
+ }
+);
+
+export const ADD_SOURCE_PLACEHOLDER = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.addSourceList.placeholder',
+ {
+ defaultMessage: 'Filter sources...',
+ }
+);
+
+export const ADD_SOURCE_EMPTY_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.addSourceList.emptyTitle',
+ {
+ defaultMessage: 'No available sources',
+ }
+);
+export const ADD_SOURCE_EMPTY_BODY = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.addSourceList.emptyBody',
+ {
+ defaultMessage:
+ 'Sources will be available for search when an administrator adds them to this organization.',
+ }
+);
+
+export const AVAILABLE_SOURCE_EMPTY_STATE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.availableSourceList.emptyState',
+ {
+ defaultMessage: 'No available sources matching your query.',
+ }
+);
+
+export const AVAILABLE_SOURCE_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.availableSourceList.title',
+ {
+ defaultMessage: 'Available for configuration',
+ }
+);
+
+export const AVAILABLE_SOURCE_BODY = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.availableSourceList.body',
+ {
+ defaultMessage: 'Configure an available source or build your own with the ',
+ }
+);
+
+export const AVAILABLE_SOURCE_CUSTOM_SOURCE_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.availableSourceList.customSource.button',
+ {
+ defaultMessage: 'Custom API Source',
+ }
+);
+
+export const CONFIG_COMPLETED_PRIVATE_SOURCES_DOCS_LINK = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configCompleted.privateDisabled.button',
+ {
+ defaultMessage: 'Learn more about private content sources.',
+ }
+);
+
+export const CONFIG_COMPLETED_CONFIGURE_NEW_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configCompleted.configureNew.button',
+ {
+ defaultMessage: 'Configure a new content source',
+ }
+);
+
+export const CONFIG_INTRO_ALT_TEXT = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configIntro.alt.text',
+ {
+ defaultMessage: 'Connection illustration',
+ }
+);
+
+export const CONFIG_INTRO_STEPS_TEXT = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configIntro.steps.text',
+ {
+ defaultMessage: 'Quick setup, then all of your documents will be searchable.',
+ }
+);
+
+export const CONFIG_INTRO_STEP1_HEADING = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configIntro.step1.heading',
+ {
+ defaultMessage: 'Step 1',
+ }
+);
+
+export const CONFIG_INTRO_STEP1_TEXT = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configIntro.step1.text',
+ {
+ defaultMessage:
+ 'Setup a secure OAuth application through the content source that you or your team will use to connect and synchronize content. You only have to do this once per content source.',
+ }
+);
+
+export const CONFIG_INTRO_STEP2_HEADING = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configIntro.step2.heading',
+ {
+ defaultMessage: 'Step 2',
+ }
+);
+
+export const CONFIG_INTRO_STEP2_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configIntro.step2.title',
+ {
+ defaultMessage: 'Connect the content source',
+ }
+);
+
+export const CONFIG_INTRO_STEP2_TEXT = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configIntro.step2.text',
+ {
+ defaultMessage:
+ 'Use the new OAuth application to connect any number of instances of the content source to Workplace Search.',
+ }
+);
+
+export const CONFIG_CUSTOM_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configCustom.button',
+ {
+ defaultMessage: 'Create Custom API Source',
+ }
+);
+
+export const CONFIG_OAUTH_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configOauth.label',
+ {
+ defaultMessage: 'Complete connection',
+ }
+);
+
+export const CONFIG_OAUTH_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configOauth.button',
+ {
+ defaultMessage: 'Complete connection',
+ }
+);
+
+export const CONFIGURED_SOURCES_LIST_UNCONNECTED_TOOLTIP = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configuredSources.unConnectedTooltip',
+ {
+ defaultMessage: 'No connected sources',
+ }
+);
+
+export const CONFIGURED_SOURCES_LIST_ACCOUNT_ONLY_TOOLTIP = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configuredSources.accountOnlyTooltip',
+ {
+ defaultMessage:
+ 'Private content source. Each user must add the content source from their own personal dashboard.',
+ }
+);
+
+export const CONFIGURED_SOURCES_CONNECT_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configuredSources.connectButton',
+ {
+ defaultMessage: 'Connect',
+ }
+);
+
+export const CONFIGURED_SOURCES_EMPTY_STATE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configuredSources.emptyState',
+ {
+ defaultMessage: 'There are no configured sources matching your query.',
+ }
+);
+
+export const CONFIGURED_SOURCES_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configuredSources.title',
+ {
+ defaultMessage: 'Configured content sources',
+ }
+);
+
+export const CONFIGURED_SOURCES_EMPTY_BODY = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.configuredSources.body',
+ {
+ defaultMessage: 'Configured and ready for connection.',
+ }
+);
+
+export const OAUTH_SAVE_CONFIG_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveConfig.button',
+ {
+ defaultMessage: 'Save configuration',
+ }
+);
+
+export const OAUTH_REMOVE_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.remove.button',
+ {
+ defaultMessage: 'Remove',
+ }
+);
+
+export const OAUTH_BACK_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.back.button',
+ {
+ defaultMessage: ' Go back',
+ }
+);
+
+export const OAUTH_STEP_2 = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveConfig.oauthStep2',
+ {
+ defaultMessage: 'Provide the appropriate configuration information',
+ }
+);
+
+export const SAVE_CUSTOM_BODY1 = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.body1',
+ {
+ defaultMessage: 'Your endpoints are ready to accept requests.',
+ }
+);
+
+export const SAVE_CUSTOM_BODY2 = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.body2',
+ {
+ defaultMessage: 'Be sure to copy your API keys below.',
+ }
+);
+
+export const SAVE_CUSTOM_RETURN_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.return.button',
+ {
+ defaultMessage: 'Return to Sources',
+ }
+);
+
+export const SAVE_CUSTOM_API_KEYS_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.apiKeys.title',
+ {
+ defaultMessage: 'API Keys',
+ }
+);
+
+export const SAVE_CUSTOM_API_KEYS_BODY = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.apiKeys.body',
+ {
+ defaultMessage: "You'll need these keys to sync documents for this custom source.",
+ }
+);
+
+export const SAVE_CUSTOM_ACCESS_TOKEN_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.accessToken.label',
+ {
+ defaultMessage: 'Access Token',
+ }
+);
+
+export const SAVE_CUSTOM_API_KEY_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.apiKey.label',
+ {
+ defaultMessage: 'Key',
+ }
+);
+
+export const SAVE_CUSTOM_VISUAL_WALKTHROUGH_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.visualWalkthrough.title',
+ {
+ defaultMessage: 'Visual Walkthrough',
+ }
+);
+
+export const SAVE_CUSTOM_STYLING_RESULTS_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.stylingResults.title',
+ {
+ defaultMessage: 'Styling Results',
+ }
+);
+
+export const SAVE_CUSTOM_DOC_PERMISSIONS_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.docPermissions.title',
+ {
+ defaultMessage: 'Set document-level permissions',
+ }
+);
+
+export const SAVE_CUSTOM_FEATURES_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.features.button',
+ {
+ defaultMessage: 'Learn about Platinum features',
+ }
+);
+
+export const SOURCE_FEATURES_SEARCHABLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.sourceFeatures.searchable.text',
+ {
+ defaultMessage: 'The following items are searchable:',
+ }
+);
+
+export const SOURCE_FEATURES_REMOTE_FEATURE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.sourceFeatures.remote.text',
+ {
+ defaultMessage:
+ 'Message data and other information is searchable in real-time from the Workplace Search experience.',
+ }
+);
+
+export const SOURCE_FEATURES_PRIVATE_FEATURE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.sourceFeatures.private.text',
+ {
+ defaultMessage:
+ 'Results returned are specific and relevant to you. Connecting this source does not expose your personal data to other search users - only you.',
+ }
+);
+
+export const SOURCE_FEATURES_GLOBAL_ACCESS_PERMISSIONS_FEATURE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.sourceFeatures.globalAccessPermissions.text',
+ {
+ defaultMessage:
+ 'All documents accessible to the connecting service user will be synchronized and made available to the organization’s users, or group’s users. Documents are immediately available for search',
+ }
+);
+
+export const SOURCE_FEATURES_DOCUMENT_LEVEL_PERMISSIONS_FEATURE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.sourceFeatures.documentLevelPermissions.text',
+ {
+ defaultMessage:
+ 'Document-level permissions manage user content access based on defined rules. Allow or deny access to certain documents for individuals and groups.',
+ }
+);
+
+export const SOURCE_FEATURES_EXPLORE_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.sourceFeatures.explore.button',
+ {
+ defaultMessage: 'Explore Platinum features',
+ }
+);
+
+export const SOURCE_FEATURES_INCLUDED_FEATURES_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.sourceFeatures.included.title',
+ {
+ defaultMessage: 'Included features',
+ }
+);
+
+export const CONNECT_REMOTE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.connect.remote.text',
+ {
+ defaultMessage: 'Remote',
+ }
+);
+
+export const CONNECT_PRIVATE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.connect.private.text',
+ {
+ defaultMessage: 'Private',
+ }
+);
+
+export const CONNECT_WHICH_OPTION_LINK = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.connect.whichOption.link',
+ {
+ defaultMessage: 'Which option should I choose?',
+ }
+);
+
+export const CONNECT_DOC_PERMISSIONS_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.connect.permissions.label',
+ {
+ defaultMessage: 'Enable document-level permission synchronization',
+ }
+);
+
+export const CONNECT_DOC_PERMISSIONS_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.connect.permissions.title',
+ {
+ defaultMessage: 'Document-level permissions',
+ }
+);
+
+export const CONNECT_NEEDS_PERMISSIONS = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.connect.needsPermissions.text',
+ {
+ defaultMessage:
+ 'Document-level permission information will be synchronized. Additional configuration is required following the initial connection before documents are available for search.',
+ }
+);
+
+export const CONNECT_NOT_SYNCED_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.connect.notSynced.title',
+ {
+ defaultMessage: 'Document-level permissions will not be synchronized',
+ }
+);
+
+export const CONNECT_NOT_SYNCED_TEXT = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.connect.notSynced.text',
+ {
+ defaultMessage:
+ 'All documents accessible to the connecting service user will be synchronized and made available to the organization’s users, or group’s users. Documents are immediately available for search. ',
+ }
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx
index 7336a3b51a444..948ea3c0bddc8 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx
@@ -9,6 +9,7 @@ import React, { useEffect, useState, FormEvent } from 'react';
import { Location } from 'history';
import { useActions, useValues } from 'kea';
import { useLocation } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui';
import { parseQueryParams } from '../../../../../../applications/shared/query_params';
@@ -58,15 +59,27 @@ export const ReAuthenticate: React.FC = ({ name, header })
>
- Your {name} credentials are no longer valid. Please re-authenticate with the original
- credentials to resume content syncing.
+ {i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.reAuthenticate.body',
+ {
+ defaultMessage:
+ 'Your {name} credentials are no longer valid. Please re-authenticate with the original credentials to resume content syncing.',
+ values: { name },
+ }
+ )}
- Re-authenticate {name}
+ {i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.reAuthenticate.button',
+ {
+ defaultMessage: 'Re-authenticate {name}',
+ values: { name },
+ }
+ )}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx
index 4036bb6a771bb..4bf46183b31e4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx
@@ -7,6 +7,7 @@
import React, { FormEvent } from 'react';
import { useActions, useValues } from 'kea';
+import { i18n } from '@kbn/i18n';
import {
EuiButton,
@@ -20,6 +21,22 @@ import {
EuiSteps,
} from '@elastic/eui';
+import {
+ PUBLIC_KEY_LABEL,
+ CONSUMER_KEY_LABEL,
+ BASE_URI_LABEL,
+ BASE_URL_LABEL,
+ CLIENT_ID_LABEL,
+ CLIENT_SECRET_LABEL,
+} from '../../../../constants';
+
+import {
+ OAUTH_SAVE_CONFIG_BUTTON,
+ OAUTH_REMOVE_BUTTON,
+ OAUTH_BACK_BUTTON,
+ OAUTH_STEP_2,
+} from './constants';
+
import { LicensingLogic } from '../../../../../../applications/shared/licensing';
import { ApiKey } from '../../../../components/shared/api_key';
@@ -76,17 +93,17 @@ export const SaveConfig: React.FC = ({
const saveButton = (
- Save configuration
+ {OAUTH_SAVE_CONFIG_BUTTON}
);
const deleteButton = (
- Remove
+ {OAUTH_REMOVE_BUTTON}
);
- const backButton = Go back ;
+ const backButton = {OAUTH_BACK_BUTTON} ;
const showSaveButton = hasPlatinumLicense || !accountContextOnly;
const formActions = (
@@ -112,10 +129,10 @@ export const SaveConfig: React.FC = ({
-
+
-
+
@@ -133,7 +150,7 @@ export const SaveConfig: React.FC = ({
const publicKeyStep2 = (
<>
-
+
= ({
-
+
= ({
name="client-id"
/>
-
+
= ({
/>
{needsBaseUrl && (
-
+
setBaseUrlValue(e.target.value)}
- name="base-uri"
+ name="base-url"
/>
)}
@@ -192,8 +209,11 @@ export const SaveConfig: React.FC = ({
);
const oauthSteps = (sourceName: string) => [
- `Create an OAuth app in your organization's ${sourceName}\u00A0account`,
- 'Provide the appropriate configuration information',
+ i18n.translate('xpack.enterpriseSearch.workplaceSearch.contentSource.saveConfig.oauthStep1', {
+ defaultMessage: "Create an OAuth app in your organization's {sourceName} account",
+ values: { sourceName },
+ }),
+ OAUTH_STEP_2,
];
const configSteps = [
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx
index 17510c3ece914..59d691023f413 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx
@@ -6,7 +6,8 @@
import React from 'react';
-import { Link } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGroup,
@@ -21,6 +22,7 @@ import {
EuiPanel,
} from '@elastic/eui';
+import { EuiLinkTo } from '../../../../../shared/react_router_helpers';
import { CredentialItem } from '../../../../components/shared/credential_item';
import { LicenseBadge } from '../../../../components/shared/license_badge';
@@ -34,6 +36,20 @@ import {
getSourcesPath,
} from '../../../../routes';
+import {
+ SAVE_CUSTOM_BODY1,
+ SAVE_CUSTOM_BODY2,
+ SAVE_CUSTOM_RETURN_BUTTON,
+ SAVE_CUSTOM_API_KEYS_TITLE,
+ SAVE_CUSTOM_API_KEYS_BODY,
+ SAVE_CUSTOM_ACCESS_TOKEN_LABEL,
+ SAVE_CUSTOM_API_KEY_LABEL,
+ SAVE_CUSTOM_VISUAL_WALKTHROUGH_TITLE,
+ SAVE_CUSTOM_STYLING_RESULTS_TITLE,
+ SAVE_CUSTOM_DOC_PERMISSIONS_TITLE,
+ SAVE_CUSTOM_FEATURES_BUTTON,
+} from './constants';
+
interface SaveCustomProps {
documentationUrl: string;
newCustomSource: CustomSource;
@@ -59,18 +75,26 @@ export const SaveCustom: React.FC = ({
- {name} Created
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.heading',
+ {
+ defaultMessage: '{name} Created',
+ values: { name },
+ }
+ )}
+
- Your endpoints are ready to accept requests.
+ {SAVE_CUSTOM_BODY1}
- Be sure to copy your API keys below.
+ {SAVE_CUSTOM_BODY2}
-
- Return to Sources
-
+
+ {SAVE_CUSTOM_RETURN_BUTTON}
+
@@ -79,15 +103,23 @@ export const SaveCustom: React.FC = ({
- API Keys
+ {SAVE_CUSTOM_API_KEYS_TITLE}
- You'll need these keys to sync documents for this custom source.
+ {SAVE_CUSTOM_API_KEYS_BODY}
-
+
-
+
@@ -98,32 +130,50 @@ export const SaveCustom: React.FC = ({
- Visual Walkthrough
+ {SAVE_CUSTOM_VISUAL_WALKTHROUGH_TITLE}
-
- Check out the documentation
- {' '}
- to learn more about Custom API Sources.
+
+ Check out the documentation
+
+ ),
+ }}
+ />
- Styling Results
+ {SAVE_CUSTOM_STYLING_RESULTS_TITLE}
- Use{' '}
-
- Display Settings
- {' '}
- to customize how your documents will appear within your search results. Workplace
- Search will use fields in alphabetical order by default.
+
+ Display Settings
+
+ ),
+ }}
+ />
@@ -133,22 +183,28 @@ export const SaveCustom: React.FC = ({
- Set document-level permissions
+ {SAVE_CUSTOM_DOC_PERMISSIONS_TITLE}
-
- Document-level permissions
- {' '}
- manage content access content on individual or group attributes. Allow or deny
- access to specific documents.
+
+ Document-level permissions
+
+ ),
+ }}
+ />
- Learn about Platinum features
+ {SAVE_CUSTOM_FEATURES_BUTTON}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx
index 6c92f3a9e13ff..b19d37c751725 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import { useValues } from 'kea';
+import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGroup,
@@ -25,6 +26,16 @@ import { LicenseBadge } from '../../../../components/shared/license_badge';
import { Features, FeatureIds } from '../../../../types';
import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../../routes';
+import {
+ SOURCE_FEATURES_SEARCHABLE,
+ SOURCE_FEATURES_REMOTE_FEATURE,
+ SOURCE_FEATURES_PRIVATE_FEATURE,
+ SOURCE_FEATURES_GLOBAL_ACCESS_PERMISSIONS_FEATURE,
+ SOURCE_FEATURES_DOCUMENT_LEVEL_PERMISSIONS_FEATURE,
+ SOURCE_FEATURES_EXPLORE_BUTTON,
+ SOURCE_FEATURES_INCLUDED_FEATURES_TITLE,
+} from './constants';
+
interface ConnectInstanceProps {
features?: Features;
objTypes?: string[];
@@ -50,8 +61,14 @@ export const SourceFeatures: React.FC = ({ features, objTy
- This source gets new content from {name} every 2 hours (following the
- initial sync).
+ 2 hours,
+ }}
+ />
@@ -61,7 +78,7 @@ export const SourceFeatures: React.FC = ({ features, objTy
<>
- The following items are searchable:
+ {SOURCE_FEATURES_SEARCHABLE}
@@ -79,7 +96,7 @@ export const SourceFeatures: React.FC = ({ features, objTy
- The following items are searchable:
+ {SOURCE_FEATURES_SEARCHABLE}
@@ -94,10 +111,7 @@ export const SourceFeatures: React.FC = ({ features, objTy
const RemoteFeature = (
-
- Message data and other information is searchable in real-time from the Workplace Search
- experience.
-
+ {SOURCE_FEATURES_REMOTE_FEATURE}
);
@@ -105,10 +119,7 @@ export const SourceFeatures: React.FC = ({ features, objTy
const PrivateFeature = (
-
- Results returned are specific and relevant to you. Connecting this source does not expose
- your personal data to other search users - only you.
-
+ {SOURCE_FEATURES_PRIVATE_FEATURE}
);
@@ -116,11 +127,7 @@ export const SourceFeatures: React.FC = ({ features, objTy
const GlobalAccessPermissionsFeature = (
-
- All documents accessible to the connecting service user will be synchronized and made
- available to the organization’s users, or group’s users. Documents are immediately
- available for search
-
+ {SOURCE_FEATURES_GLOBAL_ACCESS_PERMISSIONS_FEATURE}
);
@@ -128,12 +135,9 @@ export const SourceFeatures: React.FC = ({ features, objTy
const DocumentLevelPermissionsFeature = (
-
- Document-level permissions manage user content access based on defined rules. Allow or
- deny access to certain documents for individuals and groups.
-
+ {SOURCE_FEATURES_DOCUMENT_LEVEL_PERMISSIONS_FEATURE}
- Explore Platinum features
+ {SOURCE_FEATURES_EXPLORE_BUTTON}
@@ -170,7 +174,7 @@ export const SourceFeatures: React.FC = ({ features, objTy
return (
- Included features
+ {SOURCE_FEATURES_INCLUDED_FEATURES_TITLE}
{includedFeatures.map((featureId, i) => (
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx
index 07b4c6c298fd3..0677d46839af0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx
@@ -7,11 +7,9 @@
import React from 'react';
import { useValues } from 'kea';
-import { Link } from 'react-router-dom';
import {
EuiAvatar,
- EuiButtonEmpty,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
@@ -48,7 +46,8 @@ import { ComponentLoader } from '../../../components/shared/component_loader';
import { CredentialItem } from '../../../components/shared/credential_item';
import { ViewContentHeader } from '../../../components/shared/view_content_header';
import { LicenseBadge } from '../../../components/shared/license_badge';
-import { Loading } from '../../../../../applications/shared/loading';
+import { Loading } from '../../../../shared/loading';
+import { EuiButtonEmptyTo, EuiPanelTo } from '../../../../shared/react_router_helpers';
import aclImage from '../../../assets/supports_acl.svg';
import { SourceLogic } from '../source_logic';
@@ -116,11 +115,13 @@ export const Overview: React.FC = () => {
{totalDocuments > 0 && (
-
-
- Manage
-
-
+
+ Manage
+
)}
@@ -256,20 +257,22 @@ export const Overview: React.FC = () => {
{groups.map((group, index) => (
-
-
-
-
-
- {group.name}
-
-
-
-
-
-
-
-
+
+
+
+
+ {group.name}
+
+
+
+
+
+
+
))}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx
index f1818c852f97f..0f5f4ffb9e0da 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx
@@ -7,9 +7,8 @@
import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
-import { Link } from 'react-router-dom';
-import { EuiButton, EuiCallOut, EuiEmptyPrompt, EuiSpacer, EuiPanel } from '@elastic/eui';
+import { EuiCallOut, EuiEmptyPrompt, EuiSpacer, EuiPanel } from '@elastic/eui';
import { LicensingLogic } from '../../../../applications/shared/licensing';
@@ -18,6 +17,7 @@ import { ADD_SOURCE_PATH } from '../../routes';
import noSharedSourcesIcon from '../../assets/share_circle.svg';
import { Loading } from '../../../shared/loading';
+import { EuiButtonTo } from '../../../shared/react_router_helpers';
import { ContentSection } from '../../components/shared/content_section';
import { SourcesTable } from '../../components/shared/sources_table';
import { ViewContentHeader } from '../../components/shared/view_content_header';
@@ -79,11 +79,9 @@ export const PrivateSources: React.FC = () => {
}
const headerAction = (
-
-
- {PRIVATE_LINK_TITLE}
-
-
+
+ {PRIVATE_LINK_TITLE}
+
);
const sourcesHeader = (
diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx
index 432d2073d93b6..1c6852c5eb8d9 100644
--- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx
+++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useContext, useMemo } from 'react';
+import React, { useCallback, useContext, useMemo } from 'react';
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
@@ -26,14 +26,13 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }:
const { inventoryPrefill } = useAlertPrefillContext();
const { customMetrics } = inventoryPrefill;
-
+ const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]);
const AddAlertFlyout = useMemo(
() =>
triggersActionsUI &&
triggersActionsUI.getAddAlertFlyout({
consumer: 'infrastructure',
- addFlyoutVisible: visible!,
- setAddFlyoutVisibility: setVisible,
+ onClose: onCloseFlyout,
canChangeTrigger: false,
alertTypeId: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
metadata: {
@@ -47,5 +46,5 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }:
[triggersActionsUI, visible]
);
- return <>{AddAlertFlyout}>;
+ return <>{visible && AddAlertFlyout}>;
};
diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx
index 206621c4d4dc8..fe8493ccd0fbf 100644
--- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx
+++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useContext, useMemo } from 'react';
+import React, { useCallback, useContext, useMemo } from 'react';
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../common/alerting/logs/log_threshold/types';
@@ -14,24 +14,23 @@ interface Props {
}
export const AlertFlyout = (props: Props) => {
+ const { visible, setVisible } = props;
const { triggersActionsUI } = useContext(TriggerActionsContext);
-
+ const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]);
const AddAlertFlyout = useMemo(
() =>
triggersActionsUI &&
triggersActionsUI.getAddAlertFlyout({
consumer: 'logs',
- addFlyoutVisible: props.visible!,
- setAddFlyoutVisibility: props.setVisible,
+ onClose: onCloseFlyout,
canChangeTrigger: false,
alertTypeId: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID,
metadata: {
isInternal: true,
},
}),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [triggersActionsUI, props.visible]
+ [triggersActionsUI, onCloseFlyout]
);
- return <>{AddAlertFlyout}>;
+ return <>{visible && AddAlertFlyout}>;
};
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx
index 779478a313b71..72782f555d9ca 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useContext, useMemo } from 'react';
+import React, { useCallback, useContext, useMemo } from 'react';
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types';
@@ -19,15 +19,15 @@ interface Props {
}
export const AlertFlyout = (props: Props) => {
+ const { visible, setVisible } = props;
const { triggersActionsUI } = useContext(TriggerActionsContext);
-
+ const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]);
const AddAlertFlyout = useMemo(
() =>
triggersActionsUI &&
triggersActionsUI.getAddAlertFlyout({
consumer: 'infrastructure',
- addFlyoutVisible: props.visible!,
- setAddFlyoutVisibility: props.setVisible,
+ onClose: onCloseFlyout,
canChangeTrigger: false,
alertTypeId: METRIC_THRESHOLD_ALERT_TYPE_ID,
metadata: {
@@ -36,8 +36,8 @@ export const AlertFlyout = (props: Props) => {
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
- [triggersActionsUI, props.visible]
+ [triggersActionsUI, onCloseFlyout]
);
- return <>{AddAlertFlyout}>;
+ return <>{visible && AddAlertFlyout}>;
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss
index df73789eadedf..15464bb204f17 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss
@@ -10,6 +10,10 @@
margin-bottom: $euiSizeS;
}
+.lnsInnerIndexPatternDataPanel__titleTooltip {
+ margin-right: $euiSizeXS;
+}
+
.lnsInnerIndexPatternDataPanel__fieldItems {
// Quick fix for making sure the shadow and focus rings are visible outside the accordion bounds
padding: $euiSizeXS;
@@ -34,3 +38,5 @@
margin-right: $euiSizeS;
}
}
+
+
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
index 5121714050c68..4bb18d1ee4a17 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
@@ -335,7 +335,10 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
: i18n.translate('xpack.lens.indexPattern.availableFieldsLabel', {
defaultMessage: 'Available fields',
}),
-
+ helpText: i18n.translate('xpack.lens.indexPattern.allFieldsLabelHelp', {
+ defaultMessage:
+ 'Available fields have data in the first 500 documents that match your filters. To view all fields, expand Empty fields. Some field types cannot be visualized in Lens, including full text and geographic fields.',
+ }),
isAffectedByGlobalFilter: !!filters.length,
isAffectedByTimeFilter: true,
hideDetails: fieldInfoUnavailable,
@@ -357,6 +360,10 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
defaultNoFieldsMessage: i18n.translate('xpack.lens.indexPatterns.noEmptyDataLabel', {
defaultMessage: `There are no empty fields.`,
}),
+ helpText: i18n.translate('xpack.lens.indexPattern.emptyFieldsLabelHelp', {
+ defaultMessage:
+ 'Empty fields did not contain any values in the first 500 documents based on your filters.',
+ }),
},
MetaFields: {
fields: groupedFields.metaFields,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx
index 16d1ecbf3296b..5668e85510f9d 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx
@@ -22,6 +22,7 @@ export type FieldGroups = Record<
showInAccordion: boolean;
isInitiallyOpen: boolean;
title: string;
+ helpText?: string;
isAffectedByGlobalFilter: boolean;
isAffectedByTimeFilter: boolean;
hideDetails?: boolean;
@@ -150,6 +151,7 @@ export function FieldList({
key={key}
id={`lnsIndexPattern${key}`}
label={fieldGroup.title}
+ helpTooltip={fieldGroup.helpText}
exists={exists}
hideDetails={fieldGroup.hideDetails}
hasLoaded={!!hasSyncedExistingFields}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx
index 19f478c335784..ef249f87f05e4 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx
@@ -15,6 +15,7 @@ import {
EuiLoadingSpinner,
EuiIconTip,
} from '@elastic/eui';
+import classNames from 'classnames';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { IndexPatternField } from './types';
import { FieldItem } from './field_item';
@@ -39,6 +40,7 @@ export interface FieldsAccordionProps {
onToggle: (open: boolean) => void;
id: string;
label: string;
+ helpTooltip?: string;
hasLoaded: boolean;
fieldsCount: number;
isFiltered: boolean;
@@ -55,6 +57,7 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({
onToggle,
id,
label,
+ helpTooltip,
hasLoaded,
fieldsCount,
isFiltered,
@@ -78,6 +81,11 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({
[fieldProps, exists, hideDetails]
);
+ const titleClassname = classNames({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ lnsInnerIndexPatternDataPanel__titleTooltip: !!helpTooltip,
+ });
+
return (
- {label}
+ {label}
+ {!!helpTooltip && (
+
+ )}
}
extraAction={
diff --git a/x-pack/plugins/ml/common/types/saved_objects.ts b/x-pack/plugins/ml/common/types/saved_objects.ts
index aa3220747e849..1907078fd9752 100644
--- a/x-pack/plugins/ml/common/types/saved_objects.ts
+++ b/x-pack/plugins/ml/common/types/saved_objects.ts
@@ -18,6 +18,13 @@ export interface SyncSavedObjectResponse {
datafeedsRemoved: SavedObjectResult;
}
+export interface CanDeleteJobResponse {
+ [jobId: string]: {
+ canDelete: boolean;
+ canUntag: boolean;
+ };
+}
+
export type JobsSpacesResponse = {
[jobType in JobType]: { [jobId: string]: string[] };
};
diff --git a/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx b/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx
new file mode 100644
index 0000000000000..151946ab31fd9
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx
@@ -0,0 +1,297 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC, useState, useEffect } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiModal,
+ EuiOverlayMask,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiButton,
+ EuiLoadingSpinner,
+ EuiText,
+} from '@elastic/eui';
+import { JobType, CanDeleteJobResponse } from '../../../../common/types/saved_objects';
+import { useMlApiContext } from '../../contexts/kibana';
+import { useToastNotificationService } from '../../services/toast_notification_service';
+
+const shouldUnTagLabel = i18n.translate('xpack.ml.deleteJobCheckModal.shouldUnTagLabel', {
+ defaultMessage: 'Remove job from current space',
+});
+
+interface ModalContentReturnType {
+ buttonText: JSX.Element;
+ modalText: JSX.Element;
+}
+
+interface JobCheckRespSummary {
+ canDelete: boolean;
+ canUntag: boolean;
+ canTakeAnyAction: boolean;
+}
+
+function getRespSummary(resp: CanDeleteJobResponse): JobCheckRespSummary {
+ const jobsChecked = Object.keys(resp);
+ // Default to first job's permissions
+ const { canDelete, canUntag } = resp[jobsChecked[0]];
+ let canTakeAnyAction = true;
+
+ if (jobsChecked.length > 1) {
+ // Check all jobs and make sure they have the same permissions - otherwise no action can be taken
+ canTakeAnyAction = jobsChecked.every(
+ (id) => resp[id].canDelete === canDelete && resp[id].canUntag === canUntag
+ );
+ }
+
+ return { canDelete, canUntag, canTakeAnyAction };
+}
+
+function getModalContent(
+ jobIds: string[],
+ respSummary: JobCheckRespSummary
+): ModalContentReturnType {
+ const { canDelete, canUntag, canTakeAnyAction } = respSummary;
+
+ if (canTakeAnyAction === false) {
+ return {
+ buttonText: (
+
+ ),
+ modalText: (
+
+
+
+ ),
+ };
+ }
+
+ const noActionContent: ModalContentReturnType = {
+ buttonText: (
+
+ ),
+ modalText: (
+
+
+
+ ),
+ };
+
+ if (canDelete) {
+ return {
+ buttonText: (
+
+ ),
+ modalText: (
+
+
+
+ ),
+ };
+ } else if (canUntag) {
+ return {
+ buttonText: (
+
+ ),
+ modalText: (
+
+
+
+ ),
+ };
+ } else {
+ return noActionContent;
+ }
+}
+
+interface Props {
+ canDeleteCallback: () => void;
+ onCloseCallback: () => void;
+ refreshJobsCallback?: () => void;
+ jobType: JobType;
+ jobIds: string[];
+ setDidUntag?: React.Dispatch>;
+}
+
+export const DeleteJobCheckModal: FC = ({
+ canDeleteCallback,
+ onCloseCallback,
+ refreshJobsCallback,
+ jobType,
+ jobIds,
+ setDidUntag,
+}) => {
+ const [buttonContent, setButtonContent] = useState();
+ const [modalContent, setModalContent] = useState();
+ const [hasUntagged, setHasUntagged] = useState(false);
+ const [isUntagging, setIsUntagging] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [jobCheckRespSummary, setJobCheckRespSummary] = useState();
+
+ const {
+ savedObjects: { canDeleteJob, removeJobFromCurrentSpace },
+ } = useMlApiContext();
+ const { displayErrorToast, displaySuccessToast } = useToastNotificationService();
+
+ useEffect(() => {
+ setIsLoading(true);
+ // Do the spaces check and set the content for the modal and buttons depending on results
+ canDeleteJob(jobType, jobIds).then((resp) => {
+ const respSummary = getRespSummary(resp);
+ const { canDelete, canUntag, canTakeAnyAction } = respSummary;
+ if (canTakeAnyAction && canDelete && !canUntag) {
+ // Go straight to delete flow if that's the only action available
+ canDeleteCallback();
+ return;
+ }
+ setJobCheckRespSummary(respSummary);
+ const { buttonText, modalText } = getModalContent(jobIds, respSummary);
+ setButtonContent(buttonText);
+ setModalContent(modalText);
+ });
+ if (typeof setDidUntag === 'function') {
+ setDidUntag(false);
+ }
+ setIsLoading(false);
+ }, []);
+
+ const onUntagClick = async () => {
+ setIsUntagging(true);
+ const resp = await removeJobFromCurrentSpace(jobType, jobIds);
+ setIsUntagging(false);
+ if (typeof setDidUntag === 'function') {
+ setDidUntag(true);
+ }
+ Object.entries(resp).forEach(([id, { success, error }]) => {
+ if (success === false) {
+ const title = i18n.translate('xpack.ml.deleteJobCheckModal.unTagErrorTitle', {
+ defaultMessage: 'Error updating {id}',
+ values: { id },
+ });
+ displayErrorToast(error, title);
+ } else {
+ setHasUntagged(true);
+ const message = i18n.translate('xpack.ml.deleteJobCheckModal.unTagSuccessTitle', {
+ defaultMessage: 'Successfully updated {id}',
+ values: { id },
+ });
+ displaySuccessToast(message);
+ }
+ });
+ // Close the modal
+ onCloseCallback();
+ if (typeof refreshJobsCallback === 'function') {
+ refreshJobsCallback();
+ }
+ };
+
+ const onClick = async () => {
+ if (jobCheckRespSummary?.canTakeAnyAction && jobCheckRespSummary?.canDelete) {
+ canDeleteCallback();
+ } else {
+ onCloseCallback();
+ }
+ };
+
+ return (
+
+
+ {isLoading === true && (
+ <>
+
+
+
+
+
+
+
+ >
+ )}
+ {isLoading === false && (
+ <>
+
+
+
+
+
+
+ {modalContent}
+
+
+
+
+ {!hasUntagged &&
+ jobCheckRespSummary?.canTakeAnyAction &&
+ jobCheckRespSummary?.canUntag &&
+ jobCheckRespSummary?.canDelete && (
+
+ {shouldUnTagLabel}
+
+ )}
+
+
+
+ {buttonContent}
+
+
+
+
+ >
+ )}
+
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/components/delete_job_check_modal/index.ts b/x-pack/plugins/ml/public/application/components/delete_job_check_modal/index.ts
new file mode 100644
index 0000000000000..8fd9dc28ed8d7
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/delete_job_check_modal/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { DeleteJobCheckModal } from './delete_job_check_modal';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx
index 5db8446dec32f..07e2e9613846f 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx
@@ -23,6 +23,7 @@ export const DeleteActionModal: FC = ({
deleteTargetIndex,
deleteIndexPattern,
indexPatternExists,
+ isLoading,
item,
toggleDeleteIndex,
toggleDeleteIndexPattern,
@@ -58,6 +59,7 @@ export const DeleteActionModal: FC = ({
)}
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
buttonColor="danger"
+ confirmButtonDisabled={isLoading}
>
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx
index 5627532d3ed2c..520d410fa79f5 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx
@@ -29,17 +29,23 @@ import {
import { deleteActionNameText, DeleteActionName } from './delete_action_name';
+import { JobType } from '../../../../../../../common/types/saved_objects';
+
+const DF_ANALYTICS_JOB_TYPE: JobType = 'data-frame-analytics';
+
type DataFrameAnalyticsListRowEssentials = Pick;
export type DeleteAction = ReturnType;
export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
const [item, setItem] = useState();
- const [isModalVisible, setModalVisible] = useState(false);
+ const [isModalVisible, setModalVisible] = useState(false);
+ const [isDeleteJobCheckModalVisible, setDeleteJobCheckModalVisible] = useState(false);
const [deleteItem, setDeleteItem] = useState(false);
const [deleteTargetIndex, setDeleteTargetIndex] = useState(true);
const [deleteIndexPattern, setDeleteIndexPattern] = useState(true);
const [userCanDeleteIndex, setUserCanDeleteIndex] = useState(false);
const [indexPatternExists, setIndexPatternExists] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
const { savedObjects } = useMlKibana().services;
const savedObjectsClient = savedObjects.client;
@@ -65,8 +71,10 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
} else {
setIndexPatternExists(false);
}
+ setIsLoading(false);
} catch (e) {
const error = extractErrorMessage(e);
+ setIsLoading(false);
toastNotificationService.displayDangerToast(
i18n.translate(
@@ -88,6 +96,7 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
}
} catch (e) {
const error = extractErrorMessage(e);
+ setIsLoading(false);
toastNotificationService.displayDangerToast(
i18n.translate(
@@ -103,15 +112,16 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
};
useEffect(() => {
+ setIsLoading(true);
// Check if an index pattern exists corresponding to current DFA job
// if pattern does exist, show it to user
checkIndexPatternExists();
-
// Check if an user has permission to delete the index & index pattern
checkUserIndexPermission();
}, [isModalVisible]);
const closeModal = () => setModalVisible(false);
+ const closeDeleteJobCheckModal = () => setDeleteJobCheckModalVisible(false);
const deleteAndCloseModal = () => {
setDeleteItem(true);
setModalVisible(false);
@@ -138,6 +148,11 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
setModalVisible(true);
};
+ const openDeleteJobCheckModal = (newItem: DataFrameAnalyticsListRowEssentials) => {
+ setItem(newItem);
+ setDeleteJobCheckModalVisible(true);
+ };
+
const action: DataFrameAnalyticsListAction = useMemo(
() => ({
name: (i: DataFrameAnalyticsListRow) => (
@@ -151,7 +166,7 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
description: deleteActionNameText,
icon: 'trash',
type: 'icon',
- onClick: (i: DataFrameAnalyticsListRow) => openModal(i),
+ onClick: (i: DataFrameAnalyticsListRow) => openDeleteJobCheckModal(i),
'data-test-subj': 'mlAnalyticsJobDeleteButton',
}),
[]
@@ -159,15 +174,20 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
return {
action,
+ closeDeleteJobCheckModal,
closeModal,
deleteAndCloseModal,
deleteTargetIndex,
deleteIndexPattern,
deleteItem,
indexPatternExists,
+ isDeleteJobCheckModalVisible,
isModalVisible,
+ isLoading,
item,
+ jobType: DF_ANALYTICS_JOB_TYPE,
openModal,
+ openDeleteJobCheckModal,
toggleDeleteIndex,
toggleDeleteIndexPattern,
userCanDeleteIndex,
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx
index 74b367cc7ab13..5351c1714755e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx
@@ -10,6 +10,7 @@ import { EuiTableActionsColumnType } from '@elastic/eui';
import { checkPermission } from '../../../../../capabilities/check_capabilities';
+import { DeleteJobCheckModal } from '../../../../../components/delete_job_check_modal';
import { useCloneAction } from '../action_clone';
import { useDeleteAction, DeleteActionModal } from '../action_delete';
import { isEditActionFlyoutVisible, useEditAction, EditActionFlyout } from '../action_edit';
@@ -19,6 +20,7 @@ import { useViewAction } from '../action_view';
import { useMapAction } from '../action_map';
import { DataFrameAnalyticsListRow } from './common';
+import { useRefreshAnalyticsList } from '../../../../common/analytics';
export const useActions = (
isManagementTable: boolean
@@ -38,6 +40,8 @@ export const useActions = (
const startAction = useStartAction(canStartStopDataFrameAnalytics);
const stopAction = useStopAction(canStartStopDataFrameAnalytics);
+ const { refresh } = useRefreshAnalyticsList();
+
let modals: JSX.Element | null = null;
const actions: EuiTableActionsColumnType['actions'] = [
@@ -52,6 +56,19 @@ export const useActions = (
<>
{startAction.isModalVisible && }
{stopAction.isModalVisible && }
+ {deleteAction.isDeleteJobCheckModalVisible && deleteAction?.item?.config && (
+ {
+ // Item will always be set by the time we open the delete modal
+ deleteAction.openModal(deleteAction.item!);
+ deleteAction.closeDeleteJobCheckModal();
+ }}
+ refreshJobsCallback={refresh}
+ jobType={deleteAction.jobType}
+ jobIds={[deleteAction.item.config.id]}
+ />
+ )}
{deleteAction.isModalVisible && }
{isEditActionFlyoutVisible(editAction) && }
>
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx
index dd549ad8113f0..d307df75cb328 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx
@@ -42,13 +42,14 @@ import {
useDeleteAction,
DeleteActionModal,
} from '../../analytics_management/components/action_delete';
+import { DeleteJobCheckModal } from '../../../../components/delete_job_check_modal';
interface Props {
- analyticsId?: string;
details: any;
getNodeData: any;
modelId?: string;
updateElements: (nodeId: string, nodeLabel: string, destIndexNode?: string) => void;
+ refreshJobsCallback: () => void;
}
function getListItems(details: object): EuiDescriptionListProps['listItems'] {
@@ -74,232 +75,252 @@ function getListItems(details: object): EuiDescriptionListProps['listItems'] {
});
}
-export const Controls: FC = ({
- analyticsId,
- details,
- getNodeData,
- modelId,
- updateElements,
-}) => {
- const [showFlyout, setShowFlyout] = useState(false);
- const [selectedNode, setSelectedNode] = useState();
- const [isPopoverOpen, setPopover] = useState(false);
+export const Controls: FC = React.memo(
+ ({ details, getNodeData, modelId, refreshJobsCallback, updateElements }) => {
+ const [showFlyout, setShowFlyout] = useState(false);
+ const [selectedNode, setSelectedNode] = useState();
+ const [isPopoverOpen, setPopover] = useState(false);
+ const [didUntag, setDidUntag] = useState(false);
- const canDeleteDataFrameAnalytics: boolean = checkPermission('canDeleteDataFrameAnalytics');
- const deleteAction = useDeleteAction(canDeleteDataFrameAnalytics);
- const { deleteItem, deleteTargetIndex, isModalVisible, openModal } = deleteAction;
- const { toasts } = useNotifications();
- const mlUrlGenerator = useMlUrlGenerator();
- const navigateToPath = useNavigateToPath();
- const navigateToWizardWithClonedJob = useNavigateToWizardWithClonedJob();
+ const canDeleteDataFrameAnalytics: boolean = checkPermission('canDeleteDataFrameAnalytics');
+ const deleteAction = useDeleteAction(canDeleteDataFrameAnalytics);
+ const {
+ closeDeleteJobCheckModal,
+ deleteItem,
+ deleteTargetIndex,
+ isModalVisible,
+ isDeleteJobCheckModalVisible,
+ item,
+ jobType,
+ openModal,
+ openDeleteJobCheckModal,
+ } = deleteAction;
+ const { toasts } = useNotifications();
+ const mlUrlGenerator = useMlUrlGenerator();
+ const navigateToPath = useNavigateToPath();
+ const navigateToWizardWithClonedJob = useNavigateToWizardWithClonedJob();
- const cy = useContext(CytoscapeContext);
- const deselect = useCallback(() => {
- if (cy) {
- cy.elements().unselect();
- }
- setShowFlyout(false);
- setSelectedNode(undefined);
- }, [cy, setSelectedNode]);
+ const cy = useContext(CytoscapeContext);
+ const deselect = useCallback(() => {
+ if (cy) {
+ cy.elements().unselect();
+ }
+ setShowFlyout(false);
+ setSelectedNode(undefined);
+ }, [cy, setSelectedNode]);
- const nodeId = selectedNode?.data('id');
- const nodeLabel = selectedNode?.data('label');
- const nodeType = selectedNode?.data('type');
+ const nodeId = selectedNode?.data('id');
+ const nodeLabel = selectedNode?.data('label');
+ const nodeType = selectedNode?.data('type');
- const onCreateJobClick = useCallback(async () => {
- const indexId = getIndexPatternIdFromName(nodeLabel);
+ const onCreateJobClick = useCallback(async () => {
+ const indexId = getIndexPatternIdFromName(nodeLabel);
- if (indexId) {
- const path = await mlUrlGenerator.createUrl({
- page: ML_PAGES.DATA_FRAME_ANALYTICS_CREATE_JOB,
- pageState: { index: indexId },
- });
+ if (indexId) {
+ const path = await mlUrlGenerator.createUrl({
+ page: ML_PAGES.DATA_FRAME_ANALYTICS_CREATE_JOB,
+ pageState: { index: indexId },
+ });
- await navigateToPath(path);
- } else {
- toasts.addDanger(
- i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.indexPatternMissingMessage', {
- defaultMessage:
- 'To create a job from this index please create an index pattern for {indexTitle}.',
- values: { indexTitle: nodeLabel },
- })
- );
- }
- }, [nodeLabel]);
-
- const onCloneJobClick = useCallback(async () => {
- navigateToWizardWithClonedJob({ config: details[nodeId], stats: details[nodeId]?.stats });
- }, [nodeId]);
+ await navigateToPath(path);
+ } else {
+ toasts.addDanger(
+ i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.indexPatternMissingMessage', {
+ defaultMessage:
+ 'To create a job from this index please create an index pattern for {indexTitle}.',
+ values: { indexTitle: nodeLabel },
+ })
+ );
+ }
+ }, [nodeLabel]);
- const onActionsButtonClick = () => {
- setPopover(!isPopoverOpen);
- };
+ const onCloneJobClick = useCallback(async () => {
+ navigateToWizardWithClonedJob({ config: details[nodeId], stats: details[nodeId]?.stats });
+ }, [nodeId]);
- const closePopover = () => {
- setPopover(false);
- };
+ const onActionsButtonClick = () => {
+ setPopover(!isPopoverOpen);
+ };
- // Set up Cytoscape event handlers
- useEffect(() => {
- const selectHandler: cytoscape.EventHandler = (event) => {
- setSelectedNode(event.target);
- setShowFlyout(true);
+ const closePopover = () => {
+ setPopover(false);
};
- if (cy) {
- cy.on('select', 'node', selectHandler);
- cy.on('unselect', 'node', deselect);
- }
+ // Set up Cytoscape event handlers
+ useEffect(() => {
+ const selectHandler: cytoscape.EventHandler = (event) => {
+ setSelectedNode(event.target);
+ setShowFlyout(true);
+ };
- return () => {
if (cy) {
- cy.removeListener('select', 'node', selectHandler);
- cy.removeListener('unselect', 'node', deselect);
+ cy.on('select', 'node', selectHandler);
+ cy.on('unselect', 'node', deselect);
}
- };
- }, [cy, deselect]);
- useEffect(
- function updateElementsOnClose() {
- if (isModalVisible === false && deleteItem === true) {
- let destIndexNode;
- if (deleteTargetIndex === true) {
- const jobDetails = details[nodeId];
- const destIndex = jobDetails.dest.index;
- destIndexNode = `${destIndex}-${JOB_MAP_NODE_TYPES.INDEX}`;
+ return () => {
+ if (cy) {
+ cy.removeListener('select', 'node', selectHandler);
+ cy.removeListener('unselect', 'node', deselect);
}
- updateElements(nodeId, nodeLabel, destIndexNode);
- setShowFlyout(false);
- }
- },
- [isModalVisible, deleteItem]
- );
+ };
+ }, [cy, deselect]);
- if (showFlyout === false) {
- return null;
- }
+ useEffect(
+ function updateElementsOnClose() {
+ if ((isModalVisible === false && deleteItem === true) || didUntag === true) {
+ let destIndexNode;
+ if (deleteTargetIndex === true || didUntag === true) {
+ const jobDetails = details[nodeId];
+ const destIndex = jobDetails.dest.index;
+ destIndexNode = `${destIndex}-${JOB_MAP_NODE_TYPES.INDEX}`;
+ }
+ updateElements(nodeId, nodeLabel, destIndexNode);
+ setShowFlyout(false);
+ }
+ },
+ [isModalVisible, deleteItem, didUntag]
+ );
- const button = (
-
-
-
- );
+ if (showFlyout === false) {
+ return null;
+ }
- const items = [
- ...(nodeType === JOB_MAP_NODE_TYPES.ANALYTICS
- ? [
- {
- openModal({ config: details[nodeId], stats: details[nodeId]?.stats });
- }}
- >
-
- ,
-
-
- ,
- ]
- : []),
- ...(nodeType === JOB_MAP_NODE_TYPES.INDEX
- ? [
-
-
- ,
- ]
- : []),
- ...(analyticsId !== nodeLabel &&
- modelId !== nodeLabel &&
- (nodeType === JOB_MAP_NODE_TYPES.ANALYTICS || nodeType === JOB_MAP_NODE_TYPES.INDEX)
- ? [
- {
- getNodeData({ id: nodeLabel, type: nodeType });
- setShowFlyout(false);
- setPopover(false);
- }}
- >
-
- ,
- ]
- : []),
- ];
+ const button = (
+
+
+
+ );
- return (
-
- setShowFlyout(false)}
- data-test-subj="mlAnalyticsJobMapFlyout"
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
+ openDeleteJobCheckModal({ config: details[nodeId], stats: details[nodeId]?.stats });
+ }}
+ >
+
+ ,
+
+
-
-
-
-
- {nodeType !== JOB_MAP_NODE_TYPES.TRAINED_MODEL && (
- ,
+ ]
+ : []),
+ ...(nodeType === JOB_MAP_NODE_TYPES.INDEX
+ ? [
+
-
-
- )}
-
-
- {isModalVisible && }
-
- );
-};
+
+ ,
+ ]
+ : []),
+ ...(modelId !== nodeLabel &&
+ (nodeType === JOB_MAP_NODE_TYPES.ANALYTICS || nodeType === JOB_MAP_NODE_TYPES.INDEX)
+ ? [
+ {
+ getNodeData({ id: nodeLabel, type: nodeType });
+ setShowFlyout(false);
+ setPopover(false);
+ }}
+ >
+
+ ,
+ ]
+ : []),
+ ];
+
+ return (
+
+ setShowFlyout(false)}
+ data-test-subj="mlAnalyticsJobMapFlyout"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {nodeType !== JOB_MAP_NODE_TYPES.TRAINED_MODEL && (
+
+
+
+ )}
+
+
+ {isDeleteJobCheckModalVisible && item && (
+ {
+ // Item will always be set by the time we open the delete modal
+ openModal(deleteAction.item!);
+ closeDeleteJobCheckModal();
+ }}
+ refreshJobsCallback={refreshJobsCallback}
+ setDidUntag={setDidUntag}
+ />
+ )}
+ {isModalVisible && }
+
+ );
+ }
+);
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx
index 887d6a03757ca..7ca56617ab81d 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/job_map.tsx
@@ -127,6 +127,8 @@ export const JobMap: FC = ({ analyticsId, modelId }) => {
const { ref, width, height } = useRefDimensions();
+ const refreshCallback = () => fetchAndSetElementsWrapper({ analyticsId, modelId });
+
return (
<>
@@ -147,7 +149,7 @@ export const JobMap: FC = ({ analyticsId, modelId }) => {
fetchAndSetElementsWrapper({ analyticsId, modelId })}
+ onClick={refreshCallback}
isLoading={isLoading}
>
= ({ analyticsId, modelId }) => {
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx
new file mode 100644
index 0000000000000..90ae44eb85e5b
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx
@@ -0,0 +1,162 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC, useState, useEffect } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiSpacer,
+ EuiModal,
+ EuiOverlayMask,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiButtonEmpty,
+ EuiButton,
+ EuiLoadingSpinner,
+} from '@elastic/eui';
+
+import { deleteJobs } from '../utils';
+import { DELETING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list';
+import { DeleteJobCheckModal } from '../../../../components/delete_job_check_modal';
+
+type ShowFunc = (jobs: Array<{ id: string }>) => void;
+
+interface Props {
+ setShowFunction(showFunc: ShowFunc): void;
+ unsetShowFunction(): void;
+ refreshJobs(): void;
+}
+
+export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, refreshJobs }) => {
+ const [deleting, setDeleting] = useState(false);
+ const [modalVisible, setModalVisible] = useState(false);
+ const [jobIds, setJobIds] = useState([]);
+ const [canDelete, setCanDelete] = useState(false);
+
+ useEffect(() => {
+ if (typeof setShowFunction === 'function') {
+ setShowFunction(showModal);
+ }
+ return () => {
+ if (typeof unsetShowFunction === 'function') {
+ unsetShowFunction();
+ }
+ };
+ }, []);
+
+ function showModal(jobs: any[]) {
+ setJobIds(jobs.map(({ id }) => id));
+ setModalVisible(true);
+ setDeleting(false);
+ }
+
+ function closeModal() {
+ setModalVisible(false);
+ setCanDelete(false);
+ }
+
+ function deleteJob() {
+ setDeleting(true);
+ deleteJobs(jobIds.map((id) => ({ id })));
+
+ setTimeout(() => {
+ closeModal();
+ refreshJobs();
+ }, DELETING_JOBS_REFRESH_INTERVAL_MS);
+ }
+
+ if (modalVisible === false || jobIds.length === 0) {
+ return null;
+ }
+
+ if (canDelete) {
+ return (
+
+
+
+
+
+
+
+
+
+ {deleting === true ? (
+
+ ) : (
+ <>
+
+ >
+ )}
+
+
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+
+
+ );
+ } else {
+ return (
+ <>
+ {
+ setCanDelete(true);
+ }}
+ onCloseCallback={closeModal}
+ refreshJobsCallback={refreshJobs}
+ />
+ >
+ );
+ }
+};
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal_.js
similarity index 100%
rename from x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js
rename to x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal_.js
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/index.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/index.ts
similarity index 100%
rename from x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/index.js
rename to x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/index.ts
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts
new file mode 100644
index 0000000000000..46aa762458a24
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export function deleteJobs(jobs: Array<{ id: string }>, callback?: () => void): Promise;
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts
index e821fa3da4d66..728a258afee27 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts
@@ -11,6 +11,7 @@ import { HttpService } from '../http_service';
import { basePath } from './index';
import {
JobType,
+ CanDeleteJobResponse,
SyncSavedObjectResponse,
SavedObjectResult,
JobsSpacesResponse,
@@ -39,7 +40,14 @@ export const savedObjectsApiProvider = (httpService: HttpService) => ({
body,
});
},
-
+ removeJobFromCurrentSpace(jobType: JobType, jobIds: string[]) {
+ const body = JSON.stringify({ jobType, jobIds });
+ return httpService.http({
+ path: `${basePath()}/saved_objects/remove_job_from_current_space`,
+ method: 'POST',
+ body,
+ });
+ },
syncSavedObjects(simulate: boolean = false) {
return httpService.http({
path: `${basePath()}/saved_objects/sync`,
@@ -47,4 +55,13 @@ export const savedObjectsApiProvider = (httpService: HttpService) => ({
query: { simulate },
});
},
+
+ canDeleteJob(jobType: JobType, jobIds: string[]) {
+ const body = JSON.stringify({ jobIds });
+ return httpService.http({
+ path: `${basePath()}/saved_objects/can_delete_job/${jobType}`,
+ method: 'POST',
+ body,
+ });
+ },
});
diff --git a/x-pack/plugins/ml/server/lib/spaces_utils.ts b/x-pack/plugins/ml/server/lib/spaces_utils.ts
index ecff3b8124cf5..2ab1f52ac1584 100644
--- a/x-pack/plugins/ml/server/lib/spaces_utils.ts
+++ b/x-pack/plugins/ml/server/lib/spaces_utils.ts
@@ -52,5 +52,16 @@ export function spacesUtilsProvider(
return spaces.filter((s) => s.disabledFeatures.includes(PLUGIN_ID) === false).map((s) => s.id);
}
- return { isMlEnabledInSpace, getAllSpaces, getAllSpaceIds, getMlSpaceIds };
+ async function getCurrentSpaceId(): Promise {
+ if (getSpacesPlugin === undefined) {
+ // if spaces is disabled force isMlEnabledInSpace to be true
+ return null;
+ }
+ const space = await (await getSpacesPlugin()).spacesService.getActiveSpace(
+ request instanceof KibanaRequest ? request : KibanaRequest.from(request)
+ );
+ return space.id;
+ }
+
+ return { isMlEnabledInSpace, getAllSpaces, getAllSpaceIds, getMlSpaceIds, getCurrentSpaceId };
}
diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json
index 85df7228fe929..bf002907b1a43 100644
--- a/x-pack/plugins/ml/server/routes/apidoc.json
+++ b/x-pack/plugins/ml/server/routes/apidoc.json
@@ -149,6 +149,7 @@
"InitializeJobSavedObjects",
"AssignJobsToSpaces",
"RemoveJobsFromSpaces",
+ "RemoveJobsFromCurrentSpace",
"JobsSpaces",
"DeleteJobCheck",
diff --git a/x-pack/plugins/ml/server/routes/saved_objects.ts b/x-pack/plugins/ml/server/routes/saved_objects.ts
index 57c6084d9971b..29f9b218ea177 100644
--- a/x-pack/plugins/ml/server/routes/saved_objects.ts
+++ b/x-pack/plugins/ml/server/routes/saved_objects.ts
@@ -7,8 +7,15 @@
import { wrapError } from '../client/error_wrapper';
import { RouteInitialization, SavedObjectsRouteDeps } from '../types';
import { checksFactory, syncSavedObjectsFactory } from '../saved_objects';
-import { jobsAndSpaces, syncJobObjects, jobTypeSchema } from './schemas/saved_objects';
+import {
+ jobsAndSpaces,
+ jobsAndCurrentSpace,
+ syncJobObjects,
+ jobTypeSchema,
+} from './schemas/saved_objects';
import { jobIdsSchema } from './schemas/job_service_schema';
+import { spacesUtilsProvider } from '../lib/spaces_utils';
+import { JobType } from '../../common/types/saved_objects';
/**
* Routes for job saved object management
@@ -184,6 +191,55 @@ export function savedObjectsRoutes(
})
);
+ /**
+ * @apiGroup JobSavedObjects
+ *
+ * @api {post} /api/ml/saved_objects/remove_job_from_current_space Remove jobs from the current space
+ * @apiName RemoveJobsFromCurrentSpace
+ * @apiDescription Remove a list of jobs from the current space
+ *
+ * @apiSchema (body) jobsAndCurrentSpace
+ */
+ router.post(
+ {
+ path: '/api/ml/saved_objects/remove_job_from_current_space',
+ validate: {
+ body: jobsAndCurrentSpace,
+ },
+ options: {
+ tags: ['access:ml:canCreateJob', 'access:ml:canCreateDataFrameAnalytics'],
+ },
+ },
+ routeGuard.fullLicenseAPIGuard(async ({ request, response, jobSavedObjectService }) => {
+ try {
+ const { jobType, jobIds }: { jobType: JobType; jobIds: string[] } = request.body;
+ const { getCurrentSpaceId } = spacesUtilsProvider(getSpaces, request);
+
+ const currentSpaceId = await getCurrentSpaceId();
+ if (currentSpaceId === null) {
+ return response.ok({
+ body: jobIds.map((id) => ({
+ [id]: {
+ success: false,
+ error: 'Cannot remove current space. Spaces plugin is disabled.',
+ },
+ })),
+ });
+ }
+
+ const body = await jobSavedObjectService.removeJobsFromSpaces(jobType, jobIds, [
+ currentSpaceId,
+ ]);
+
+ return response.ok({
+ body,
+ });
+ } catch (e) {
+ return response.customError(wrapError(e));
+ }
+ })
+ );
+
/**
* @apiGroup JobSavedObjects
*
diff --git a/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts b/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts
index c2d091bd16052..147398694f191 100644
--- a/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts
+++ b/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts
@@ -12,6 +12,11 @@ export const jobsAndSpaces = schema.object({
spaces: schema.arrayOf(schema.string()),
});
+export const jobsAndCurrentSpace = schema.object({
+ jobType: schema.string(),
+ jobIds: schema.arrayOf(schema.string()),
+});
+
export const syncJobObjects = schema.object({ simulate: schema.maybe(schema.boolean()) });
export const jobTypeSchema = schema.object({
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts
index 09c6db5f16acf..d57afae92ff99 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts
@@ -413,6 +413,7 @@ export const threat_tactic = t.type({
name: threat_tactic_name,
reference: threat_tactic_reference,
});
+export type ThreatTactic = t.TypeOf;
export const threat_subtechnique_id = t.string;
export const threat_subtechnique_name = t.string;
export const threat_subtechnique_reference = t.string;
@@ -421,6 +422,7 @@ export const threat_subtechnique = t.type({
name: threat_subtechnique_name,
reference: threat_subtechnique_reference,
});
+export type ThreatSubtechnique = t.TypeOf;
export const threat_subtechniques = t.array(threat_subtechnique);
export const threat_technique_id = t.string;
export const threat_technique_name = t.string;
@@ -439,21 +441,22 @@ export const threat_technique = t.intersection([
})
),
]);
+export type ThreatTechnique = t.TypeOf;
export const threat_techniques = t.array(threat_technique);
-export const threat = t.array(
- t.exact(
- t.type({
- framework: threat_framework,
- tactic: threat_tactic,
- technique: threat_techniques,
- })
- )
+export const threat = t.exact(
+ t.type({
+ framework: threat_framework,
+ tactic: threat_tactic,
+ technique: threat_techniques,
+ })
);
-
export type Threat = t.TypeOf;
-export const threatOrUndefined = t.union([threat, t.undefined]);
-export type ThreatOrUndefined = t.TypeOf;
+export const threats = t.array(threat);
+export type Threats = t.TypeOf;
+
+export const threatsOrUndefined = t.union([threats, t.undefined]);
+export type ThreatsOrUndefined = t.TypeOf;
export const threshold = t.exact(
t.type({
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts
index 1b0417cf59bc2..0fe8a04382f97 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts
@@ -23,7 +23,7 @@ import {
Tags,
To,
type,
- Threat,
+ Threats,
threshold,
ThrottleOrNull,
note,
@@ -171,7 +171,7 @@ export type AddPrepackagedRulesSchemaDecoded = Omit<
severity_mapping: SeverityMapping;
tags: Tags;
to: To;
- threat: Threat;
+ threat: Threats;
throttle: ThrottleOrNull;
exceptions_list: ListArray;
};
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts
index 4f28c46923865..3a9c3e7d96971 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts
@@ -25,7 +25,7 @@ import {
Tags,
To,
type,
- Threat,
+ Threats,
threshold,
ThrottleOrNull,
note,
@@ -193,7 +193,7 @@ export type ImportRulesSchemaDecoded = Omit<
severity_mapping: SeverityMapping;
tags: Tags;
to: To;
- threat: Threat;
+ threat: Threats;
throttle: ThrottleOrNull;
version: Version;
exceptions_list: ListArray;
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts
index 45fcfbaa3c76a..6229fc9e7b915 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts
@@ -31,7 +31,7 @@ import {
from,
enabled,
tags,
- threat,
+ threats,
threshold,
throttle,
references,
@@ -98,7 +98,7 @@ export const patchRulesSchema = t.exact(
severity_mapping,
tags,
to,
- threat,
+ threat: threats,
threshold,
throttle,
timestamp_override,
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts
index 799b06f169df5..d6c1783f6ea1f 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts
@@ -42,7 +42,7 @@ import {
max_signals,
risk_score,
severity,
- threat,
+ threats,
to,
references,
version,
@@ -167,7 +167,7 @@ const commonParams = {
max_signals,
risk_score_mapping,
severity_mapping,
- threat,
+ threat: threats,
to,
references,
version,
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts
index 0f7d04763a36f..19f095f59506e 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts
@@ -43,7 +43,7 @@ import {
timeline_id,
timeline_title,
type,
- threat,
+ threats,
threshold,
throttle,
job_status,
@@ -106,7 +106,7 @@ export const requiredRulesSchema = t.type({
tags,
to,
type,
- threat,
+ threat: threats,
created_at,
updated_at,
created_by,
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.test.ts
index 42193128cccfa..935657acac3b1 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.test.ts
@@ -8,11 +8,11 @@ import { DefaultThreatArray } from './default_threat_array';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
-import { Threat } from '../common/schemas';
+import { Threats } from '../common/schemas';
describe('default_threat_null', () => {
test('it should validate an empty array', () => {
- const payload: Threat = [];
+ const payload: Threats = [];
const decoded = DefaultThreatArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
@@ -21,7 +21,7 @@ describe('default_threat_null', () => {
});
test('it should validate an array of threats', () => {
- const payload: Threat = [
+ const payload: Threats = [
{
framework: 'MITRE ATTACK',
technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }],
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts
index 3b3c20f003e6e..e42dda07aeaa3 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts
@@ -6,16 +6,16 @@
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
-import { Threat, threat } from '../common/schemas';
+import { Threats, threats } from '../common/schemas';
/**
* Types the DefaultThreatArray as:
* - If null or undefined, then an empty array will be set
*/
-export const DefaultThreatArray = new t.Type(
+export const DefaultThreatArray = new t.Type(
'DefaultThreatArray',
- threat.is,
- (input, context): Either =>
- input == null ? t.success([]) : threat.validate(input, context),
+ threats.is,
+ (input, context): Either =>
+ input == null ? t.success([]) : threats.validate(input, context),
t.identity
);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat.mock.ts
index 083c902dea9d4..5b22a5975c695 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat.mock.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat.mock.ts
@@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Threat } from '../common/schemas';
+import { Threats } from '../common/schemas';
-export const getThreatMock = (): Threat => [
+export const getThreatMock = (): Threats => [
{
framework: 'MITRE ATT&CK',
tactic: {
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx
index 3ab23266abf52..37ce8a7ea7c4f 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx
@@ -18,11 +18,7 @@ import {
} from '../../../../../../../../src/plugins/data/public';
import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations';
import { useKibana } from '../../../../common/lib/kibana';
-import {
- AboutStepRiskScore,
- AboutStepSeverity,
- IMitreEnterpriseAttack,
-} from '../../../pages/detection_engine/rules/types';
+import { AboutStepRiskScore, AboutStepSeverity } from '../../../pages/detection_engine/rules/types';
import { FieldValueTimeline } from '../pick_timeline';
import { FormSchema } from '../../../../shared_imports';
import { ListItems } from './types';
@@ -42,7 +38,7 @@ import {
import { buildMlJobDescription } from './ml_job_description';
import { buildActionsDescription } from './actions_description';
import { buildThrottleDescription } from './throttle_description';
-import { Type } from '../../../../../common/detection_engine/schemas/common/schemas';
+import { Threats, Type } from '../../../../../common/detection_engine/schemas/common/schemas';
import { THREAT_QUERY_LABEL } from './translations';
import { filterEmptyThreats } from '../../../pages/detection_engine/rules/create/helpers';
@@ -179,7 +175,7 @@ export const getDescriptionItem = (
indexPatterns,
});
} else if (field === 'threat') {
- const threats: IMitreEnterpriseAttack[] = get(field, data);
+ const threats: Threats = get(field, data);
return buildThreatDescription({ label, threat: filterEmptyThreats(threats) });
} else if (field === 'threshold') {
const threshold = get(field, data);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts
index 719c38689b722..5cee44d1dba2f 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts
@@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ReactNode } from 'react';
+import { Threats } from '../../../../../common/detection_engine/schemas/common/schemas';
import {
IIndexPattern,
Filter,
FilterManager,
} from '../../../../../../../../src/plugins/data/public';
-import { IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types';
export interface ListItems {
title: NonNullable;
@@ -29,5 +29,5 @@ export interface BuildQueryBarDescription {
export interface BuildThreatDescription {
label: string;
- threat: IMitreEnterpriseAttack[];
+ threat: Threats;
}
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx
index bb117641bdee9..1fbb6461bf43e 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx
@@ -5,51 +5,11 @@
*/
import { getValidThreat } from '../../../mitre/valid_threat_mock';
-import { hasSubtechniqueOptions, isMitreAttackInvalid } from './helpers';
+import { hasSubtechniqueOptions } from './helpers';
const mockTechniques = getValidThreat()[0].technique;
describe('helpers', () => {
- describe('isMitreAttackInvalid', () => {
- describe('when technique param is undefined', () => {
- it('returns false', () => {
- expect(isMitreAttackInvalid('', undefined)).toBe(false);
- });
- });
-
- describe('when technique param is empty', () => {
- it('returns false if tacticName is `none`', () => {
- expect(isMitreAttackInvalid('none', [])).toBe(false);
- });
-
- it('returns true if tacticName exists and is not `none`', () => {
- expect(isMitreAttackInvalid('Test', [])).toBe(true);
- });
- });
-
- describe('when technique param exists', () => {
- describe('and contains valid techniques', () => {
- const validTechniques = mockTechniques;
- it('returns false', () => {
- expect(isMitreAttackInvalid('Test', validTechniques)).toBe(false);
- });
- });
-
- describe('and contains empty techniques', () => {
- const emptyTechniques = [
- {
- reference: 'https://test.com',
- name: 'none',
- id: '',
- },
- ];
- it('returns true', () => {
- expect(isMitreAttackInvalid('Test', emptyTechniques)).toBe(true);
- });
- });
- });
- });
-
describe('hasSubtechniqueOptions', () => {
describe('when technique has subtechnique options', () => {
const technique = mockTechniques[0];
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts
index eb0ebd50398ac..b2bdb9dcc737c 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts
@@ -3,32 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { isEmpty } from 'lodash/fp';
+import { ThreatTechnique } from '../../../../../common/detection_engine/schemas/common/schemas';
import { subtechniquesOptions } from '../../../mitre/mitre_tactics_techniques';
-import { IMitreAttackTechnique } from '../../../pages/detection_engine/rules/types';
-
-export const isMitreAttackInvalid = (
- tacticName: string | null | undefined,
- technique: IMitreAttackTechnique[] | null | undefined
-) => {
- if (
- tacticName !== 'none' &&
- technique != null &&
- (isEmpty(technique) || !containsTechniques(technique))
- ) {
- return true;
- }
- return false;
-};
-
-const containsTechniques = (techniques: IMitreAttackTechnique[]) => {
- return techniques.some((technique) => technique.name !== 'none');
-};
-
/**
* Returns true if the given mitre technique has any subtechniques
*/
-export const hasSubtechniqueOptions = (technique: IMitreAttackTechnique) => {
+export const hasSubtechniqueOptions = (technique: ThreatTechnique) => {
return subtechniquesOptions.some((subtechnique) => subtechnique.techniqueId === technique.id);
};
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx
index e5918cb065f39..e3a1265f57798 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx
@@ -6,19 +6,18 @@
import { EuiButtonIcon, EuiFormRow, EuiSuperSelect, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { isEmpty, camelCase } from 'lodash/fp';
-import React, { memo, useCallback, useMemo, useState } from 'react';
+import React, { memo, useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { isEqual } from 'lodash';
+import { Threat, Threats } from '../../../../../common/detection_engine/schemas/common/schemas';
import { tacticsOptions } from '../../../mitre/mitre_tactics_techniques';
import * as Rulei18n from '../../../pages/detection_engine/rules/translations';
-import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports';
+import { FieldHook } from '../../../../shared_imports';
import { threatDefault } from '../step_about_rule/default_value';
-import { IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types';
import { MyAddItemButton } from '../add_item_form';
import * as i18n from './translations';
import { MitreAttackTechniqueFields } from './technique_fields';
-import { isMitreAttackInvalid } from './helpers';
const MitreAttackContainer = styled.div`
margin-top: 16px;
@@ -40,12 +39,9 @@ interface AddItemProps {
}
export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItemProps) => {
- const [showValidation, setShowValidation] = useState(false);
- const { errorMessage } = getFieldValidityAndErrorMessage(field);
-
const removeTactic = useCallback(
(index: number) => {
- const values = [...(field.value as IMitreEnterpriseAttack[])];
+ const values = [...(field.value as Threats)];
values.splice(index, 1);
if (isEmpty(values)) {
field.setValue(threatDefault);
@@ -57,7 +53,7 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
);
const addMitreAttackTactic = useCallback(() => {
- const values = [...(field.value as IMitreEnterpriseAttack[])];
+ const values = [...(field.value as Threats)];
if (!isEmpty(values[values.length - 1])) {
field.setValue([
...values,
@@ -70,7 +66,7 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
const updateTactic = useCallback(
(index: number, value: string) => {
- const values = [...(field.value as IMitreEnterpriseAttack[])];
+ const values = [...(field.value as Threats)];
const { id, reference, name } = tacticsOptions.find((t) => t.value === value) || {
id: '',
name: '',
@@ -87,15 +83,11 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
);
const values = useMemo(() => {
- return [...(field.value as IMitreEnterpriseAttack[])];
+ return [...(field.value as Threats)];
}, [field]);
- const isTacticValid = useCallback((threat: IMitreEnterpriseAttack) => {
- return isMitreAttackInvalid(threat.tactic.name, threat.technique);
- }, []);
-
const getSelectTactic = useCallback(
- (threat: IMitreEnterpriseAttack, index: number, disabled: boolean) => {
+ (threat: Threat, index: number, disabled: boolean) => {
const tacticName = threat.tactic.name;
return (
@@ -125,8 +117,6 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
valueOfSelected={camelCase(tacticName)}
data-test-subj="mitreAttackTactic"
placeholder={i18n.TACTIC_PLACEHOLDER}
- isInvalid={showValidation && isTacticValid(threat)}
- onBlur={() => setShowValidation(true)}
/>
@@ -141,7 +131,7 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
);
},
- [field, isDisabled, removeTactic, showValidation, updateTactic, values, isTacticValid]
+ [field, isDisabled, removeTactic, updateTactic, values]
);
/**
@@ -150,7 +140,7 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
* Value is memoized on top level props, any deep changes will have to be new objects
*/
const onFieldChange = useCallback(
- (threats: IMitreEnterpriseAttack[]) => {
+ (threats: Threats) => {
field.setValue(threats);
},
[field]
@@ -166,16 +156,12 @@ export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItem
label={`${field.label} ${i18n.THREATS}`}
labelAppend={field.labelAppend}
describedByIds={idAria ? [`${idAria} ${i18n.TACTIC}`] : undefined}
- isInvalid={showValidation && isTacticValid(threat)}
- error={errorMessage}
>
<>{getSelectTactic(threat, index, isDisabled)}>
) : (
{getSelectTactic(threat, index, isDisabled)}
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx
index bc4226ca23ca8..6329e3d510c34 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx
@@ -16,10 +16,13 @@ import { camelCase } from 'lodash/fp';
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
+import {
+ Threats,
+ ThreatSubtechnique,
+} from '../../../../../common/detection_engine/schemas/common/schemas';
import { subtechniquesOptions } from '../../../mitre/mitre_tactics_techniques';
import * as Rulei18n from '../../../pages/detection_engine/rules/translations';
import { FieldHook } from '../../../../shared_imports';
-import { IMitreAttack, IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types';
import { MyAddItemButton } from '../add_item_form';
import * as i18n from './translations';
@@ -33,7 +36,7 @@ interface AddSubtechniqueProps {
techniqueIndex: number;
idAria: string;
isDisabled: boolean;
- onFieldChange: (threats: IMitreEnterpriseAttack[]) => void;
+ onFieldChange: (threats: Threats) => void;
}
export const MitreAttackSubtechniqueFields: React.FC = ({
@@ -44,7 +47,7 @@ export const MitreAttackSubtechniqueFields: React.FC = ({
techniqueIndex,
onFieldChange,
}): JSX.Element => {
- const values = field.value as IMitreEnterpriseAttack[];
+ const values = field.value as Threats;
const technique = useMemo(() => {
return values[threatIndex].technique[techniqueIndex];
@@ -52,7 +55,7 @@ export const MitreAttackSubtechniqueFields: React.FC = ({
const removeSubtechnique = useCallback(
(index: number) => {
- const threats = [...(field.value as IMitreEnterpriseAttack[])];
+ const threats = [...(field.value as Threats)];
const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique;
if (subtechniques != null) {
subtechniques.splice(index, 1);
@@ -68,7 +71,7 @@ export const MitreAttackSubtechniqueFields: React.FC = ({
);
const addMitreAttackSubtechnique = useCallback(() => {
- const threats = [...(field.value as IMitreEnterpriseAttack[])];
+ const threats = [...(field.value as Threats)];
const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique;
@@ -89,7 +92,7 @@ export const MitreAttackSubtechniqueFields: React.FC = ({
const updateSubtechnique = useCallback(
(index: number, value: string) => {
- const threats = [...(field.value as IMitreEnterpriseAttack[])];
+ const threats = [...(field.value as Threats)];
const { id, reference, name } = subtechniquesOptions.find((t) => t.value === value) || {
id: '',
name: '',
@@ -127,7 +130,7 @@ export const MitreAttackSubtechniqueFields: React.FC = ({
);
const getSelectSubtechnique = useCallback(
- (index: number, disabled: boolean, subtechnique: IMitreAttack) => {
+ (index: number, disabled: boolean, subtechnique: ThreatSubtechnique) => {
const options = subtechniquesOptions.filter((t) => t.techniqueId === technique.id);
return (
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/technique_fields.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/technique_fields.tsx
index c9d8623d16e82..46ef6abf65927 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/technique_fields.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/technique_fields.tsx
@@ -16,13 +16,13 @@ import { kebabCase, camelCase } from 'lodash/fp';
import React, { useCallback } from 'react';
import styled, { css } from 'styled-components';
+import {
+ Threats,
+ ThreatTechnique,
+} from '../../../../../common/detection_engine/schemas/common/schemas';
import { techniquesOptions } from '../../../mitre/mitre_tactics_techniques';
import * as Rulei18n from '../../../pages/detection_engine/rules/translations';
import { FieldHook } from '../../../../shared_imports';
-import {
- IMitreAttackTechnique,
- IMitreEnterpriseAttack,
-} from '../../../pages/detection_engine/rules/types';
import { MyAddItemButton } from '../add_item_form';
import { hasSubtechniqueOptions } from './helpers';
import * as i18n from './translations';
@@ -41,7 +41,7 @@ interface AddTechniqueProps {
threatIndex: number;
idAria: string;
isDisabled: boolean;
- onFieldChange: (threats: IMitreEnterpriseAttack[]) => void;
+ onFieldChange: (threats: Threats) => void;
}
export const MitreAttackTechniqueFields: React.FC = ({
@@ -51,11 +51,11 @@ export const MitreAttackTechniqueFields: React.FC = ({
threatIndex,
onFieldChange,
}): JSX.Element => {
- const values = field.value as IMitreEnterpriseAttack[];
+ const values = field.value as Threats;
const removeTechnique = useCallback(
(index: number) => {
- const threats = [...(field.value as IMitreEnterpriseAttack[])];
+ const threats = [...(field.value as Threats)];
const techniques = threats[threatIndex].technique;
techniques.splice(index, 1);
threats[threatIndex] = {
@@ -68,7 +68,7 @@ export const MitreAttackTechniqueFields: React.FC = ({
);
const addMitreAttackTechnique = useCallback(() => {
- const threats = [...(field.value as IMitreEnterpriseAttack[])];
+ const threats = [...(field.value as Threats)];
threats[threatIndex] = {
...threats[threatIndex],
technique: [
@@ -81,7 +81,7 @@ export const MitreAttackTechniqueFields: React.FC = ({
const updateTechnique = useCallback(
(index: number, value: string) => {
- const threats = [...(field.value as IMitreEnterpriseAttack[])];
+ const threats = [...(field.value as Threats)];
const { id, reference, name } = techniquesOptions.find((t) => t.value === value) || {
id: '',
name: '',
@@ -109,7 +109,7 @@ export const MitreAttackTechniqueFields: React.FC = ({
);
const getSelectTechnique = useCallback(
- (tacticName: string, index: number, disabled: boolean, technique: IMitreAttackTechnique) => {
+ (tacticName: string, index: number, disabled: boolean, technique: ThreatTechnique) => {
const options = techniquesOptions.filter((t) => t.tactics.includes(kebabCase(tacticName)));
return (
<>
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx
index eb30d23ca50ff..7cb7134975350 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx
@@ -13,8 +13,7 @@ import {
ValidationFunc,
ERROR_CODE,
} from '../../../../shared_imports';
-import { IMitreEnterpriseAttack, AboutStepRule } from '../../../pages/detection_engine/rules/types';
-import { isMitreAttackInvalid } from '../mitre/helpers';
+import { AboutStepRule } from '../../../pages/detection_engine/rules/types';
import { OptionalFieldLabel } from '../optional_field_label';
import { isUrlInvalid } from '../../../../common/utils/validators';
import * as I18n from './translations';
@@ -192,29 +191,6 @@ export const schema: FormSchema = {
}
),
labelAppend: OptionalFieldLabel,
- validations: [
- {
- validator: (
- ...args: Parameters
- ): ReturnType> | undefined => {
- const [{ value, path }] = args;
- let hasTechniqueError = false;
- (value as IMitreEnterpriseAttack[]).forEach((v) => {
- if (isMitreAttackInvalid(v.tactic.name, v.technique)) {
- hasTechniqueError = true;
- }
- });
- return hasTechniqueError
- ? {
- code: 'ERR_FIELD_MISSING',
- path: `${path}.tactic`,
- message: I18n.CUSTOM_MITRE_ATTACK_TECHNIQUES_REQUIRED,
- }
- : undefined;
- },
- exitOnFail: false,
- },
- ],
},
timestampOverride: {
type: FIELD_TYPES.TEXT,
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts
index f4d90d0596ede..939b8052f74e0 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts
@@ -69,13 +69,6 @@ export const CRITICAL = i18n.translate(
}
);
-export const CUSTOM_MITRE_ATTACK_TECHNIQUES_REQUIRED = i18n.translate(
- 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customMitreAttackTechniquesFieldRequiredError',
- {
- defaultMessage: 'At least one Technique is required with a Tactic.',
- }
-);
-
export const URL_FORMAT_INVALID = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.referencesUrlInvalidError',
{
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
index d7908a6780ebb..b930212610ae9 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
@@ -17,6 +17,7 @@ import {
timestamp_override,
threshold,
type,
+ threats,
} from '../../../../../common/detection_engine/schemas/common/schemas';
import {
listArray,
@@ -77,6 +78,7 @@ const StatusTypes = t.union([
t.literal('partial failure'),
]);
+// TODO: make a ticket
export const RuleSchema = t.intersection([
t.type({
author,
@@ -100,7 +102,7 @@ export const RuleSchema = t.intersection([
tags: t.array(t.string),
type,
to: t.string,
- threat: t.array(t.unknown),
+ threat: threats,
updated_at: t.string,
updated_by: t.string,
actions: t.array(action),
diff --git a/x-pack/plugins/security_solution/public/detections/mitre/valid_threat_mock.ts b/x-pack/plugins/security_solution/public/detections/mitre/valid_threat_mock.ts
index 1694ff3fddd3b..3be4b49d8b4e6 100644
--- a/x-pack/plugins/security_solution/public/detections/mitre/valid_threat_mock.ts
+++ b/x-pack/plugins/security_solution/public/detections/mitre/valid_threat_mock.ts
@@ -4,14 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Threat } from '../../../common/detection_engine/schemas/common/schemas';
+import { Threats } from '../../../common/detection_engine/schemas/common/schemas';
import { mockThreatData } from './mitre_tactics_techniques';
const { tactic, technique, subtechnique } = mockThreatData;
const { tactics, ...mockTechnique } = technique;
const { tactics: subtechniqueTactics, ...mockSubtechnique } = subtechnique;
-export const getValidThreat = (): Threat => [
+export const getValidThreat = (): Threats => [
{
framework: 'MITRE ATT&CK',
tactic,
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts
index 1b4934cf7c9ec..8451eb0dfbe6c 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts
@@ -20,7 +20,6 @@ import {
ActionsStepRule,
ScheduleStepRule,
DefineStepRule,
- IMitreEnterpriseAttack,
} from '../types';
import {
getTimeTypeValue,
@@ -40,6 +39,7 @@ import {
mockActionsStepRule,
} from '../all/__mocks__/mock';
import { getThreatMock } from '../../../../../../common/detection_engine/schemas/types/threat.mock';
+import { Threat, Threats } from '../../../../../../common/detection_engine/schemas/common/schemas';
describe('helpers', () => {
describe('getTimeTypeValue', () => {
@@ -87,14 +87,14 @@ describe('helpers', () => {
});
describe('filterEmptyThreats', () => {
- let mockThreat: IMitreEnterpriseAttack;
+ let mockThreat: Threat;
beforeEach(() => {
mockThreat = mockAboutStepRule().threat[0];
});
test('filters out fields with empty tactics', () => {
- const threat: IMitreEnterpriseAttack[] = [
+ const threat: Threats = [
mockThreat,
{ ...mockThreat, tactic: { ...mockThreat.tactic, name: 'none' } },
];
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
index 4f25c33fad92d..7952bd396b72a 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
@@ -14,7 +14,12 @@ import { transformAlertToRuleAction } from '../../../../../../common/detection_e
import { List } from '../../../../../../common/detection_engine/schemas/types';
import { ENDPOINT_LIST_ID, ExceptionListType, NamespaceType } from '../../../../../shared_imports';
import { Rule } from '../../../../containers/detection_engine/rules';
-import { Type } from '../../../../../../common/detection_engine/schemas/common/schemas';
+import {
+ Threats,
+ ThreatSubtechnique,
+ ThreatTechnique,
+ Type,
+} from '../../../../../../common/detection_engine/schemas/common/schemas';
import {
AboutStepRule,
@@ -27,9 +32,6 @@ import {
ActionsStepRuleJson,
RuleStepsFormData,
RuleStep,
- IMitreEnterpriseAttack,
- IMitreAttack,
- IMitreAttackTechnique,
} from '../types';
export const getTimeTypeValue = (time: string): { unit: string; value: number } => {
@@ -164,7 +166,7 @@ export const filterRuleFieldsForType = >(
assertUnreachable(type);
};
-function trimThreatsWithNoName(
+function trimThreatsWithNoName(
filterable: T[]
): T[] {
return filterable.filter((item) => item.name !== 'none');
@@ -173,7 +175,7 @@ function trimThreatsWithNoName(
/**
* Filter out unfilled/empty threat, technique, and subtechnique fields based on if their name is `none`
*/
-export const filterEmptyThreats = (threats: IMitreEnterpriseAttack[]): IMitreEnterpriseAttack[] => {
+export const filterEmptyThreats = (threats: Threats): Threats => {
return threats
.filter((singleThreat) => singleThreat.tactic.name !== 'none')
.map((threat) => {
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx
index 74549827b6fa2..f22dade2579a6 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx
@@ -22,7 +22,6 @@ import {
AboutStepRule,
AboutStepRuleDetails,
DefineStepRule,
- IMitreEnterpriseAttack,
ScheduleStepRule,
ActionsStepRule,
} from './types';
@@ -30,6 +29,7 @@ import {
SeverityMapping,
Type,
Severity,
+ Threats,
} from '../../../../../common/detection_engine/schemas/common/schemas';
import { severityOptions } from '../../../components/rules/step_about_rule/data';
@@ -177,7 +177,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu
isMappingChecked: riskScoreMapping.length > 0,
},
falsePositives,
- threat: threat as IMitreEnterpriseAttack[],
+ threat: threat as Threats,
};
};
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts
index 5fe529a5b77bb..59cc7fba017e2 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts
@@ -21,6 +21,7 @@ import {
TimestampOverride,
Type,
Severity,
+ Threats,
} from '../../../../../common/detection_engine/schemas/common/schemas';
import {
List,
@@ -99,7 +100,7 @@ export interface AboutStepRule {
ruleNameOverride: string;
tags: string[];
timestampOverride: string;
- threat: IMitreEnterpriseAttack[];
+ threat: Threats;
note: string;
}
@@ -178,7 +179,7 @@ export interface AboutStepRuleJson {
false_positives: string[];
rule_name_override?: RuleNameOverride;
tags: string[];
- threat: IMitreEnterpriseAttack[];
+ threat: Threats;
timestamp_override?: TimestampOverride;
note?: string;
}
@@ -196,22 +197,3 @@ export interface ActionsStepRuleJson {
throttle?: string | null;
meta?: unknown;
}
-
-export interface IMitreAttack {
- id: string;
- name: string;
- reference: string;
-}
-
-export interface IMitreAttackTechnique {
- id: string;
- name: string;
- reference: string;
- subtechnique?: IMitreAttack[];
-}
-
-export interface IMitreEnterpriseAttack {
- framework: string;
- tactic: IMitreAttack;
- technique: IMitreAttackTechnique[];
-}
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx
index 4d3ccaf278c91..c462bd1e3553e 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.test.tsx
@@ -15,7 +15,7 @@ import { urlSearch } from '../../test_utilities/url_search';
const resolverComponentInstanceID = 'resolverComponentInstanceID';
// FLAKY: https://github.com/elastic/kibana/issues/85714
-describe(`Resolver: when analyzing a tree with only the origin and paginated related events, and when the component instance ID is ${resolverComponentInstanceID}`, () => {
+describe.skip(`Resolver: when analyzing a tree with only the origin and paginated related events, and when the component instance ID is ${resolverComponentInstanceID}`, () => {
/**
* Get (or lazily create and get) the simulator.
*/
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
index 45186f9978650..aaeb487ad9a78 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
@@ -28,7 +28,7 @@ import {
Name,
Severity,
Tags,
- Threat,
+ Threats,
To,
Type,
References,
@@ -59,7 +59,7 @@ import {
SeverityOrUndefined,
TagsOrUndefined,
ToOrUndefined,
- ThreatOrUndefined,
+ ThreatsOrUndefined,
ThresholdOrUndefined,
TypeOrUndefined,
ReferencesOrUndefined,
@@ -231,7 +231,7 @@ export interface CreateRulesOptions {
severity: Severity;
severityMapping: SeverityMapping;
tags: Tags;
- threat: Threat;
+ threat: Threats;
threshold: ThresholdOrUndefined;
threatFilters: ThreatFiltersOrUndefined;
threatIndex: ThreatIndexOrUndefined;
@@ -288,7 +288,7 @@ export interface PatchRulesOptions {
severity: SeverityOrUndefined;
severityMapping: SeverityMappingOrUndefined;
tags: TagsOrUndefined;
- threat: ThreatOrUndefined;
+ threat: ThreatsOrUndefined;
itemsPerSearch: ItemsPerSearchOrUndefined;
concurrentSearches: ConcurrentSearchesOrUndefined;
threshold: ThresholdOrUndefined;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts
index 613e8e474079c..ee1dbe6104e8b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts
@@ -28,7 +28,7 @@ import {
SeverityOrUndefined,
TagsOrUndefined,
ToOrUndefined,
- ThreatOrUndefined,
+ ThreatsOrUndefined,
ThresholdOrUndefined,
TypeOrUndefined,
ReferencesOrUndefined,
@@ -93,7 +93,7 @@ export interface UpdateProperties {
severity: SeverityOrUndefined;
severityMapping: SeverityMappingOrUndefined;
tags: TagsOrUndefined;
- threat: ThreatOrUndefined;
+ threat: ThreatsOrUndefined;
threshold: ThresholdOrUndefined;
threatFilters: ThreatFiltersOrUndefined;
threatIndex: ThreatIndexOrUndefined;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts
index 0af9d6ac4377d..9eea7b226f94f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts
@@ -43,7 +43,7 @@ import {
severityMappingOrUndefined,
tags,
timestampOverrideOrUndefined,
- threat,
+ threats,
to,
references,
version,
@@ -85,7 +85,7 @@ export const baseRuleParams = t.exact(
severity,
severityMapping: severityMappingOrUndefined,
timestampOverride: timestampOverrideOrUndefined,
- threat,
+ threat: threats,
to,
references,
version,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts
index 5cac76e2b0c01..57535178c5280 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts
@@ -8,7 +8,7 @@ import {
AnomalyThresholdOrUndefined,
Description,
NoteOrUndefined,
- ThreatOrUndefined,
+ ThreatsOrUndefined,
ThresholdOrUndefined,
FalsePositives,
From,
@@ -82,7 +82,7 @@ export interface RuleTypeParams {
ruleNameOverride: RuleNameOverrideOrUndefined;
severity: Severity;
severityMapping: SeverityMappingOrUndefined;
- threat: ThreatOrUndefined;
+ threat: ThreatsOrUndefined;
threshold: ThresholdOrUndefined;
threatFilters: PartialFilter[] | undefined;
threatIndex: ThreatIndexOrUndefined;
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 99e8ed84ba99b..3b4b97aa31fcf 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -16668,7 +16668,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionHighDescription": "高",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription": "低",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionMediumDescription": "中",
- "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customMitreAttackTechniquesFieldRequiredError": "Tacticには1つ以上のTechniqueが必要です。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldInvalidError": "KQLが無効です",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError": "カスタムクエリが必要です。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "すべての一致には、フィールドと脅威インデックスフィールドの両方が必要です。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 32f61b37b7b8d..a45336de97fff 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -16685,7 +16685,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionHighDescription": "高",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription": "低",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionMediumDescription": "中",
- "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customMitreAttackTechniquesFieldRequiredError": "一个策略至少需要一个技术。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldInvalidError": "KQL 无效",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError": "需要定制查询。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "所有匹配项都需要字段和威胁索引字段。",
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx
index e068b1274b89a..2790ea8aa6bfa 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx
@@ -129,8 +129,7 @@ describe('alert_add', () => {
wrapper = mountWithIntl(
{}}
+ onClose={() => {}}
initialValues={initialValues}
reloadAlerts={() => {
return new Promise(() => {});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx
index 5ab2c7f5a586c..c432f68e71ef4 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx
@@ -26,10 +26,9 @@ import { useKibana } from '../../../common/lib/kibana';
export interface AlertAddProps> {
consumer: string;
- addFlyoutVisible: boolean;
alertTypeRegistry: AlertTypeRegistryContract;
actionTypeRegistry: ActionTypeRegistryContract;
- setAddFlyoutVisibility: React.Dispatch>;
+ onClose: () => void;
alertTypeId?: string;
canChangeTrigger?: boolean;
initialValues?: Partial;
@@ -39,10 +38,9 @@ export interface AlertAddProps> {
const AlertAdd = ({
consumer,
- addFlyoutVisible,
alertTypeRegistry,
actionTypeRegistry,
- setAddFlyoutVisibility,
+ onClose,
canChangeTrigger,
alertTypeId,
initialValues,
@@ -92,9 +90,9 @@ const AlertAdd = ({
}, [alertTypeId]);
const closeFlyout = useCallback(() => {
- setAddFlyoutVisibility(false);
setAlert(initialAlert);
- }, [initialAlert, setAddFlyoutVisibility]);
+ onClose();
+ }, [initialAlert, onClose]);
const saveAlertAndCloseFlyout = async () => {
const savedAlert = await onSaveAlert();
@@ -107,10 +105,6 @@ const AlertAdd = ({
}
};
- if (!addFlyoutVisible) {
- return null;
- }
-
const alertType = alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null;
const errors = {
...(alertType ? alertType.validate(alert.params).errors : []),
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx
index a576d811e9f8d..7df5c6e157106 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx
@@ -26,6 +26,7 @@ jest.mock('../../../lib/action_connector_api', () => ({
jest.mock('../../../lib/alert_api', () => ({
loadAlerts: jest.fn(),
loadAlertTypes: jest.fn(),
+ health: jest.fn(() => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true })),
}));
jest.mock('react-router-dom', () => ({
useHistory: () => ({
@@ -115,7 +116,16 @@ describe('alerts_list component empty', () => {
expect(
wrapper.find('[data-test-subj="createFirstAlertButton"]').find('EuiButton')
).toHaveLength(1);
- expect(wrapper.find('AlertAdd')).toHaveLength(1);
+ expect(wrapper.find('AlertAdd').exists()).toBeFalsy();
+
+ wrapper.find('button[data-test-subj="createFirstAlertButton"]').simulate('click');
+
+ // When the AlertAdd component is rendered, it waits for the healthcheck to resolve
+ await new Promise((resolve) => {
+ setTimeout(resolve, 1000);
+ });
+ wrapper.update();
+ expect(wrapper.find('AlertAdd').exists()).toEqual(true);
});
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx
index bf0d97d418d5c..1369e6e8f3b82 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx
@@ -676,14 +676,17 @@ export const AlertsList: React.FunctionComponent = () => {
) : (
noPermissionPrompt
)}
-
+ {alertFlyoutVisible && (
+ {
+ setAlertFlyoutVisibility(false);
+ }}
+ actionTypeRegistry={actionTypeRegistry}
+ alertTypeRegistry={alertTypeRegistry}
+ reloadAlerts={loadAlertsData}
+ />
+ )}
);
};
diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_step.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_step.test.tsx
index 6864dc0eb7cf5..e3a3d39241de2 100644
--- a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_step.test.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_step.test.tsx
@@ -9,7 +9,8 @@ import { ExecutedStep } from '../executed_step';
import { Ping } from '../../../../../common/runtime_types';
import { mountWithRouter } from '../../../../lib';
-describe('ExecutedStep', () => {
+// FLAKY: https://github.com/elastic/kibana/issues/85899
+describe.skip('ExecutedStep', () => {
let step: Ping;
beforeEach(() => {
diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx
index 7995cf88df9ba..75cbd43cd0b38 100644
--- a/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useMemo } from 'react';
+import React, { useCallback, useMemo } from 'react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { TriggersAndActionsUIPublicPluginStart } from '../../../../../../plugins/triggers_actions_ui/public';
@@ -24,19 +24,20 @@ export const UptimeAlertsFlyoutWrapperComponent = ({
setAlertFlyoutVisibility,
}: Props) => {
const { triggersActionsUi } = useKibana().services;
-
+ const onCloseAlertFlyout = useCallback(() => setAlertFlyoutVisibility(false), [
+ setAlertFlyoutVisibility,
+ ]);
const AddAlertFlyout = useMemo(
() =>
triggersActionsUi.getAddAlertFlyout({
consumer: 'uptime',
- addFlyoutVisible: alertFlyoutVisible,
- setAddFlyoutVisibility: setAlertFlyoutVisibility,
+ onClose: onCloseAlertFlyout,
alertTypeId,
canChangeTrigger: !alertTypeId,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
- [alertFlyoutVisible, alertTypeId]
+ [onCloseAlertFlyout, alertTypeId]
);
- return <>{AddAlertFlyout}>;
+ return <>{alertFlyoutVisible && AddAlertFlyout}>;
};
diff --git a/x-pack/test/functional/services/ml/job_table.ts b/x-pack/test/functional/services/ml/job_table.ts
index 54c03c876af8a..c3b998d61fb31 100644
--- a/x-pack/test/functional/services/ml/job_table.ts
+++ b/x-pack/test/functional/services/ml/job_table.ts
@@ -316,7 +316,7 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte
}
public async confirmDeleteJobModal() {
- await testSubjects.click('mlDeleteJobConfirmModal > confirmModalConfirmButton');
+ await testSubjects.click('mlDeleteJobConfirmModal > mlDeleteJobConfirmModalButton');
await testSubjects.missingOrFail('mlDeleteJobConfirmModal', { timeout: 30 * 1000 });
}