From 3e211e938bc4c110fd4ab21e38f2b3f133a546ee Mon Sep 17 00:00:00 2001
From: John Schulz
Date: Mon, 9 Nov 2020 05:42:45 -0500
Subject: [PATCH 1/9] [Ingest Manager] Unify install* under installPackage
(#82916)
## Summary
* Add `installPackage` with `installSource` param, to provide a single interface the `install*` functions.
```diff
- const res = await installPackageFromRegistry({
+ const res = await installPackage({
+ installSource: 'registry',
```
and
```diff
- const res = await installPackageByUpload({
+ const res = await installPackage({
+ installSource: 'upload',
```
* Push some repeated work (`install`, `removable`) from `install*` into `_installPackage`. Which also simplifies its interface.
### installPackage
For now `installPackage` checks the `installSource` and calls the same `install*` functions to prevent any change in behavior but there's still a lot of overlap between `installPackageFromRegistry` & `installPackageByUpload`. I think we can bring them together into `installPackage` using the same branching on `installSource`.
### local checks with curl
curl request/responses for happy path:
```
## zip:
curl -X POST -u elastic:changeme http://localhost:5601/api/fleet/epm/packages --data-binary @$KIBANA_HOME/x-pack/test/ingest_manager_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.zip -H 'kbn-xsrf: xyz' -H 'Content-Type: application/zip'
{"response":[{"id":"apache-Logs-Apache-Dashboard-ecs","type":"dashboard"},{"id":"apache-Metrics-Apache-HTTPD-server-status-ecs","type":"dashboard"},{"id":"Apache-HTTPD-CPU-ecs","type":"visualization"},{"id":"Apache-HTTPD-Hostname-list-ecs","type":"visualization"},{"id":"Apache-HTTPD-Load1-slash-5-slash-15-ecs","type":"visualization"},{"id":"Apache-HTTPD-Scoreboard-ecs","type":"visualization"},{"id":"Apache-HTTPD-Total-accesses-and-kbytes-ecs","type":"visualization"},{"id":"Apache-HTTPD-Uptime-ecs","type":"visualization"},{"id":"Apache-HTTPD-Workers-ecs","type":"visualization"},{"id":"Apache-access-unique-IPs-map-ecs","type":"visualization"},{"id":"Apache-browsers-ecs","type":"visualization"},{"id":"Apache-error-logs-over-time-ecs","type":"visualization"},{"id":"Apache-operating-systems-ecs","type":"visualization"},{"id":"Apache-response-codes-of-top-URLs-ecs","type":"visualization"},{"id":"Apache-response-codes-over-time-ecs","type":"visualization"},{"id":"Apache-HTTPD-ecs","type":"search"},{"id":"Apache-access-logs-ecs","type":"search"},{"id":"Apache-errors-log-ecs","type":"search"}]}
## Uploaded packages can be deleted as expected:
curl -X DELETE -u elastic:changeme http://localhost:5601/api/fleet/epm/packages/apache-0.1.4 -H 'kbn-xsrf: xxx'
{"response":[{"id":"apache-Logs-Apache-Dashboard-ecs","type":"dashboard"},{"id":"apache-Metrics-Apache-HTTPD-server-status-ecs","type":"dashboard"},{"id":"Apache-HTTPD-CPU-ecs","type":"visualization"},{"id":"Apache-HTTPD-Hostname-list-ecs","type":"visualization"},{"id":"Apache-HTTPD-Load1-slash-5-slash-15-ecs","type":"visualization"},{"id":"Apache-HTTPD-Scoreboard-ecs","type":"visualization"},{"id":"Apache-HTTPD-Total-accesses-and-kbytes-ecs","type":"visualization"},{"id":"Apache-HTTPD-Uptime-ecs","type":"visualization"},{"id":"Apache-HTTPD-Workers-ecs","type":"visualization"},{"id":"Apache-access-unique-IPs-map-ecs","type":"visualization"},{"id":"Apache-browsers-ecs","type":"visualization"},{"id":"Apache-error-logs-over-time-ecs","type":"visualization"},{"id":"Apache-operating-systems-ecs","type":"visualization"},{"id":"Apache-response-codes-of-top-URLs-ecs","type":"visualization"},{"id":"Apache-response-codes-over-time-ecs","type":"visualization"},{"id":"Apache-HTTPD-ecs","type":"search"},{"id":"Apache-access-logs-ecs","type":"search"},{"id":"Apache-errors-log-ecs","type":"search"}]}
## Now upload
curl -X POST -u elastic:changeme http://localhost:5601/api/fleet/epm/packages --data-binary @$KIBANA_HOME/x-pack/test/ingest_manager_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz -H 'kbn-xsrf: xyz' -H 'Content-Type: application/gzip'
{"response":[{"id":"apache-Metrics-Apache-HTTPD-server-status-ecs","type":"dashboard"},{"id":"apache-Logs-Apache-Dashboard-ecs","type":"dashboard"},{"id":"Apache-access-unique-IPs-map-ecs","type":"visualization"},{"id":"Apache-HTTPD-CPU-ecs","type":"visualization"},{"id":"Apache-HTTPD-Load1-slash-5-slash-15-ecs","type":"visualization"},{"id":"Apache-response-codes-over-time-ecs","type":"visualization"},{"id":"Apache-HTTPD-Workers-ecs","type":"visualization"},{"id":"Apache-HTTPD-Hostname-list-ecs","type":"visualization"},{"id":"Apache-error-logs-over-time-ecs","type":"visualization"},{"id":"Apache-HTTPD-Scoreboard-ecs","type":"visualization"},{"id":"Apache-HTTPD-Uptime-ecs","type":"visualization"},{"id":"Apache-operating-systems-ecs","type":"visualization"},{"id":"Apache-HTTPD-Total-accesses-and-kbytes-ecs","type":"visualization"},{"id":"Apache-browsers-ecs","type":"visualization"},{"id":"Apache-response-codes-of-top-URLs-ecs","type":"visualization"},{"id":"Apache-access-logs-ecs","type":"search"},{"id":"Apache-errors-log-ecs","type":"search"},{"id":"Apache-HTTPD-ecs","type":"search"},{"id":"logs-apache.error-0.1.4","type":"ingest_pipeline"},{"id":"logs-apache.access-0.1.4","type":"ingest_pipeline"},{"id":"logs-apache.error","type":"index_template"},{"id":"metrics-apache.status","type":"index_template"},{"id":"logs-apache.access","type":"index_template"}]}
```
curl request/responses for archive errors:
```
## Wrong content type:
### tar.gz with application/zip:
curl -X POST -u elastic:changeme http://localhost:5601/api/fleet/epm/packages --data-binary @$KIBANA_HOME/x-pack/test/ingest_manager_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz -H 'kbn-xsrf: xyz' -H 'Content-Type: application/zip'
{"statusCode":400,"error":"Bad Request","message":"Error during extraction of package: Error: end of central directory record signature not found. Assumed content type was application/zip, check if this matches the archive type."}
### zip with application/gzip:
curl -X POST -u elastic:changeme http://localhost:5601/api/fleet/epm/packages --data-binary @$KIBANA_HOME/x-pack/test/ingest_manager_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.zip -H 'kbn-xsrf: xyz' -H 'Content-Type: application/gzip'
{"statusCode":400,"error":"Bad Request","message":"Archive seems empty. Assumed content type was application/gzip, check if this matches the archive type."}
## Invalid packages
### Two top-level directories:
curl -X POST -u elastic:changeme http://localhost:5601/api/fleet/epm/packages --data-binary @$KIBANA_HOME/x-pack/test/ingest_manager_api_integration/apis/fixtures/direct_upload_packages/apache_invalid_two_toplevels_0.1.4.zip -H 'kbn-xsrf: xyz' -H 'Content-Type: application/zip'
{"statusCode":400,"error":"Bad Request","message":"Package contains more than one top-level directory."}
### No manifest:
curl -X POST -u elastic:changeme http://localhost:5601/api/fleet/epm/packages --data-binary @$KIBANA_HOME/x-pack/test/ingest_manager_api_integration/apis/fixtures/direct_upload_packages/apache_invalid_no_manifest_0.1.4.zip -H 'kbn-xsrf: xyz' -H 'Content-Type: application/zip'
{"statusCode":400,"error":"Bad Request","message":"Package must contain a top-level manifest.yml file."}
### Invalid YAML in manifest:
curl -X POST -u elastic:changeme http://localhost:5601/api/fleet/epm/packages --data-binary @$KIBANA_HOME/x-pack/test/ingest_manager_api_integration/apis/fixtures/direct_upload_packages/apache_invalid_manifest_invalid_yaml_0.1.4.zip -H 'kbn-xsrf: xyz' -H 'Content-Type: application/zip'
{"statusCode":400,"error":"Bad Request","message":"Could not parse top-level package manifest: YAMLException: bad indentation of a mapping entry at line 2, column 7:\n name: apache\n ^."}
### Mandatory field missing in manifest:
curl -X POST -u elastic:changeme http://localhost:5601/api/fleet/epm/packages --data-binary @$KIBANA_HOME/x-pack/test/ingest_manager_api_integration/apis/fixtures/direct_upload_packages/apache_invalid_manifest_missing_field_0.1.4.zip -H 'kbn-xsrf: xyz' -H 'Content-Type: application/zip'
{"statusCode":400,"error":"Bad Request","message":"Invalid top-level package manifest: one or more fields missing of name, version, description, type, categories, format_version"}
### Top-level directory doesn't match name and version from manifest:
curl -X POST -u elastic:changeme http://localhost:5601/api/fleet/epm/packages --data-binary @$KIBANA_HOME/x-pack/test/ingest_manager_api_integration/apis/fixtures/direct_upload_packages/apache_invalid_toplevel_mismatch_0.1.4.zip -H 'kbn-xsrf: xyz' -H 'Content-Type: application/zip'
{"statusCode":400,"error":"Bad Request","message":"Name thisIsATypo and version 0.1.4 do not match top-level directory apache-0.1.4"}
```
#### TS type check examples on `installPackage`
screenshots
### Checklist
- [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
---
.../server/routes/epm/handlers.ts | 9 +-
.../epm/packages/_install_package.test.ts | 4 -
.../services/epm/packages/_install_package.ts | 11 +--
.../server/services/epm/packages/index.ts | 3 +-
.../server/services/epm/packages/install.ts | 92 ++++++++++++-------
5 files changed, 68 insertions(+), 51 deletions(-)
diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
index 198a54ca84125..1d221b8b1eead 100644
--- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
@@ -35,8 +35,7 @@ import {
getPackageInfo,
handleInstallPackageFailure,
isBulkInstallError,
- installPackageFromRegistry,
- installPackageByUpload,
+ installPackage,
removeInstallation,
getLimitedPackages,
getInstallationObject,
@@ -149,7 +148,8 @@ export const installPackageFromRegistryHandler: RequestHandler<
const { pkgName, pkgVersion } = splitPkgKey(pkgkey);
const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
try {
- const res = await installPackageFromRegistry({
+ const res = await installPackage({
+ installSource: 'registry',
savedObjectsClient,
pkgkey,
callCluster,
@@ -224,7 +224,8 @@ export const installPackageByUploadHandler: RequestHandler<
const contentType = request.headers['content-type'] as string; // from types it could also be string[] or undefined but this is checked later
const archiveBuffer = Buffer.from(request.body);
try {
- const res = await installPackageByUpload({
+ const res = await installPackage({
+ installSource: 'upload',
savedObjectsClient,
callCluster,
archiveBuffer,
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.test.ts
index 5d3e8e9ce87d1..b7650d10b6b25 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.test.ts
@@ -61,11 +61,7 @@ describe('_installPackage', () => {
const installationPromise = _installPackage({
savedObjectsClient: soClient,
callCluster,
- pkgName: 'abc',
- pkgVersion: '1.2.3',
paths: [],
- removable: false,
- internal: false,
packageInfo: {
name: 'xyz',
version: '4.5.6',
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.ts
index f570984cc61aa..a83d9428b7c93 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.ts
@@ -21,6 +21,7 @@ import { installPipelines, deletePreviousPipelines } from '../elasticsearch/inge
import { installILMPolicy } from '../elasticsearch/ilm/install';
import { installKibanaAssets, getKibanaAssets } from '../kibana/assets/install';
import { updateCurrentWriteIndices } from '../elasticsearch/template/template';
+import { isRequiredPackage } from './index';
import { deleteKibanaSavedObjectsAssets } from './remove';
import { installTransform } from '../elasticsearch/transform/install';
import { createInstallation, saveKibanaAssetsRefs, updateVersion } from './install';
@@ -32,28 +33,22 @@ import { createInstallation, saveKibanaAssetsRefs, updateVersion } from './insta
export async function _installPackage({
savedObjectsClient,
callCluster,
- pkgName,
- pkgVersion,
installedPkg,
paths,
- removable,
- internal,
packageInfo,
installType,
installSource,
}: {
savedObjectsClient: SavedObjectsClientContract;
callCluster: CallESAsCurrentUser;
- pkgName: string;
- pkgVersion: string;
installedPkg?: SavedObject;
paths: string[];
- removable: boolean;
- internal: boolean;
packageInfo: InstallablePackage;
installType: InstallType;
installSource: InstallSource;
}): Promise {
+ const { internal = false, name: pkgName, version: pkgVersion } = packageInfo;
+ const removable = !isRequiredPackage(pkgName);
const toSaveESIndexPatterns = generateESIndexPatterns(packageInfo.data_streams);
// add the package installation to the saved object.
// if some installation already exists, just update install info
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts
index 410a9c0b22537..a1128011d81e6 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts
@@ -29,8 +29,7 @@ export {
BulkInstallResponse,
IBulkInstallPackageError,
handleInstallPackageFailure,
- installPackageFromRegistry,
- installPackageByUpload,
+ installPackage,
ensureInstalledPackage,
} from './install';
export { removeInstallation } from './remove';
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
index e7d8c8d4695d4..00a5c689e906d 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
@@ -24,7 +24,6 @@ import * as Registry from '../registry';
import {
getInstallation,
getInstallationObject,
- isRequiredPackage,
bulkInstallPackages,
isBulkInstallError,
} from './index';
@@ -52,7 +51,7 @@ export async function installLatestPackage(options: {
name: latestPackage.name,
version: latestPackage.version,
});
- return installPackageFromRegistry({ savedObjectsClient, pkgkey, callCluster });
+ return installPackage({ installSource: 'registry', savedObjectsClient, pkgkey, callCluster });
} catch (err) {
throw err;
}
@@ -148,7 +147,8 @@ export async function handleInstallPackageFailure({
}
const prevVersion = `${pkgName}-${installedPkg.attributes.version}`;
logger.error(`rolling back to ${prevVersion} after error installing ${pkgkey}`);
- await installPackageFromRegistry({
+ await installPackage({
+ installSource: 'registry',
savedObjectsClient,
pkgkey: prevVersion,
callCluster,
@@ -186,7 +186,12 @@ export async function upgradePackage({
});
try {
- const assets = await installPackageFromRegistry({ savedObjectsClient, pkgkey, callCluster });
+ const assets = await installPackage({
+ installSource: 'registry',
+ savedObjectsClient,
+ pkgkey,
+ callCluster,
+ });
return {
name: pkgToUpgrade,
newVersion: latestPkg.version,
@@ -218,19 +223,19 @@ export async function upgradePackage({
}
}
-interface InstallPackageParams {
+interface InstallRegistryPackageParams {
savedObjectsClient: SavedObjectsClientContract;
pkgkey: string;
callCluster: CallESAsCurrentUser;
force?: boolean;
}
-export async function installPackageFromRegistry({
+async function installPackageFromRegistry({
savedObjectsClient,
pkgkey,
callCluster,
force = false,
-}: InstallPackageParams): Promise {
+}: InstallRegistryPackageParams): Promise {
// TODO: change epm API to /packageName/version so we don't need to do this
const { pkgName, pkgVersion } = Registry.splitPkgKey(pkgkey);
// TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge
@@ -250,37 +255,36 @@ export async function installPackageFromRegistry({
const { paths, registryPackageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion);
- const removable = !isRequiredPackage(pkgName);
- const { internal = false } = registryPackageInfo;
- const installSource = 'registry';
-
return _installPackage({
savedObjectsClient,
callCluster,
- pkgName,
- pkgVersion,
installedPkg,
paths,
- removable,
- internal,
packageInfo: registryPackageInfo,
installType,
- installSource,
+ installSource: 'registry',
});
}
-export async function installPackageByUpload({
- savedObjectsClient,
- callCluster,
- archiveBuffer,
- contentType,
-}: {
+interface InstallUploadedArchiveParams {
savedObjectsClient: SavedObjectsClientContract;
callCluster: CallESAsCurrentUser;
archiveBuffer: Buffer;
contentType: string;
-}): Promise {
+}
+
+export type InstallPackageParams =
+ | ({ installSource: Extract } & InstallRegistryPackageParams)
+ | ({ installSource: Extract } & InstallUploadedArchiveParams);
+
+async function installPackageByUpload({
+ savedObjectsClient,
+ callCluster,
+ archiveBuffer,
+ contentType,
+}: InstallUploadedArchiveParams): Promise {
const { paths, archivePackageInfo } = await loadArchivePackage({ archiveBuffer, contentType });
+
const installedPkg = await getInstallationObject({
savedObjectsClient,
pkgName: archivePackageInfo.name,
@@ -292,25 +296,45 @@ export async function installPackageByUpload({
);
}
- const removable = !isRequiredPackage(archivePackageInfo.name);
- const { internal = false } = archivePackageInfo;
- const installSource = 'upload';
-
return _installPackage({
savedObjectsClient,
callCluster,
- pkgName: archivePackageInfo.name,
- pkgVersion: archivePackageInfo.version,
installedPkg,
paths,
- removable,
- internal,
packageInfo: archivePackageInfo,
installType,
- installSource,
+ installSource: 'upload',
});
}
+export async function installPackage(args: InstallPackageParams) {
+ if (!('installSource' in args)) {
+ throw new Error('installSource is required');
+ }
+
+ if (args.installSource === 'registry') {
+ const { savedObjectsClient, pkgkey, callCluster, force } = args;
+
+ return installPackageFromRegistry({
+ savedObjectsClient,
+ pkgkey,
+ callCluster,
+ force,
+ });
+ } else if (args.installSource === 'upload') {
+ const { savedObjectsClient, callCluster, archiveBuffer, contentType } = args;
+
+ return installPackageByUpload({
+ savedObjectsClient,
+ callCluster,
+ archiveBuffer,
+ contentType,
+ });
+ }
+ // @ts-expect-error s/b impossibe b/c `never` by this point, but just in case
+ throw new Error(`Unknown installSource: ${args.installSource}`);
+}
+
export const updateVersion = async (
savedObjectsClient: SavedObjectsClientContract,
pkgName: string,
@@ -421,7 +445,9 @@ export async function ensurePackagesCompletedInstall(
const pkgkey = `${pkg.attributes.name}-${pkg.attributes.install_version}`;
// reinstall package
if (elapsedTime > MAX_TIME_COMPLETE_INSTALL) {
- acc.push(installPackageFromRegistry({ savedObjectsClient, pkgkey, callCluster }));
+ acc.push(
+ installPackage({ installSource: 'registry', savedObjectsClient, pkgkey, callCluster })
+ );
}
return acc;
}, []);
From fdc18392ad2c70b92fbfb4f69e1af892c020efab Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Mon, 9 Nov 2020 11:51:14 +0100
Subject: [PATCH 2/9] SavedObjects search_dsl: add match_phrase_prefix clauses
when using prefix search (#82693)
* add match_phrase_prefix clauses when using prefix search
* add FTR tests
---
.../lib/search_dsl/query_params.test.ts | 473 ++++++++++++------
.../service/lib/search_dsl/query_params.ts | 189 +++++--
.../service/lib/search_dsl/search_dsl.test.ts | 1 -
.../service/lib/search_dsl/search_dsl.ts | 1 -
.../apis/saved_objects/find.js | 64 +++
.../saved_objects/find_edgecases/data.json | 93 ++++
.../find_edgecases/mappings.json | 267 ++++++++++
7 files changed, 883 insertions(+), 205 deletions(-)
create mode 100644 test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/data.json
create mode 100644 test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/mappings.json
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts
index 333f5caf72525..a8c5df8d64630 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts
@@ -21,28 +21,64 @@
import { esKuery } from '../../../es_query';
type KueryNode = any;
-import { typeRegistryMock } from '../../../saved_objects_type_registry.mock';
+import { SavedObjectTypeRegistry } from '../../../saved_objects_type_registry';
import { ALL_NAMESPACES_STRING } from '../utils';
import { getQueryParams, getClauseForReference } from './query_params';
-const registry = typeRegistryMock.create();
+const registerTypes = (registry: SavedObjectTypeRegistry) => {
+ registry.registerType({
+ name: 'pending',
+ hidden: false,
+ namespaceType: 'single',
+ mappings: {
+ properties: { title: { type: 'text' } },
+ },
+ management: {
+ defaultSearchField: 'title',
+ },
+ });
-const MAPPINGS = {
- properties: {
- pending: { properties: { title: { type: 'text' } } },
- saved: {
+ registry.registerType({
+ name: 'saved',
+ hidden: false,
+ namespaceType: 'single',
+ mappings: {
properties: {
title: { type: 'text', fields: { raw: { type: 'keyword' } } },
obj: { properties: { key1: { type: 'text' } } },
},
},
- // mock registry returns isMultiNamespace=true for 'shared' type
- shared: { properties: { name: { type: 'keyword' } } },
- // mock registry returns isNamespaceAgnostic=true for 'global' type
- global: { properties: { name: { type: 'keyword' } } },
- },
+ management: {
+ defaultSearchField: 'title',
+ },
+ });
+
+ registry.registerType({
+ name: 'shared',
+ hidden: false,
+ namespaceType: 'multiple',
+ mappings: {
+ properties: { name: { type: 'keyword' } },
+ },
+ management: {
+ defaultSearchField: 'name',
+ },
+ });
+
+ registry.registerType({
+ name: 'global',
+ hidden: false,
+ namespaceType: 'agnostic',
+ mappings: {
+ properties: { name: { type: 'keyword' } },
+ },
+ management: {
+ defaultSearchField: 'name',
+ },
+ });
};
-const ALL_TYPES = Object.keys(MAPPINGS.properties);
+
+const ALL_TYPES = ['pending', 'saved', 'shared', 'global'];
// get all possible subsets (combination) of all types
const ALL_TYPE_SUBSETS = ALL_TYPES.reduce(
(subsets, value) => subsets.concat(subsets.map((set) => [...set, value])),
@@ -51,48 +87,53 @@ const ALL_TYPE_SUBSETS = ALL_TYPES.reduce(
.filter((x) => x.length) // exclude empty set
.map((x) => (x.length === 1 ? x[0] : x)); // if a subset is a single string, destructure it
-const createTypeClause = (type: string, namespaces?: string[]) => {
- if (registry.isMultiNamespace(type)) {
- const array = [...(namespaces ?? ['default']), ALL_NAMESPACES_STRING];
- return {
- bool: {
- must: expect.arrayContaining([{ terms: { namespaces: array } }]),
- must_not: [{ exists: { field: 'namespace' } }],
- },
- };
- } else if (registry.isSingleNamespace(type)) {
- const nonDefaultNamespaces = namespaces?.filter((n) => n !== 'default') ?? [];
- const should: any = [];
- if (nonDefaultNamespaces.length > 0) {
- should.push({ terms: { namespace: nonDefaultNamespaces } });
- }
- if (namespaces?.includes('default')) {
- should.push({ bool: { must_not: [{ exists: { field: 'namespace' } }] } });
- }
- return {
- bool: {
- must: [{ term: { type } }],
- should: expect.arrayContaining(should),
- minimum_should_match: 1,
- must_not: [{ exists: { field: 'namespaces' } }],
- },
- };
- }
- // isNamespaceAgnostic
- return {
- bool: expect.objectContaining({
- must_not: [{ exists: { field: 'namespace' } }, { exists: { field: 'namespaces' } }],
- }),
- };
-};
-
/**
* Note: these tests cases are defined in the order they appear in the source code, for readability's sake
*/
describe('#getQueryParams', () => {
- const mappings = MAPPINGS;
+ let registry: SavedObjectTypeRegistry;
type Result = ReturnType;
+ beforeEach(() => {
+ registry = new SavedObjectTypeRegistry();
+ registerTypes(registry);
+ });
+
+ const createTypeClause = (type: string, namespaces?: string[]) => {
+ if (registry.isMultiNamespace(type)) {
+ const array = [...(namespaces ?? ['default']), ALL_NAMESPACES_STRING];
+ return {
+ bool: {
+ must: expect.arrayContaining([{ terms: { namespaces: array } }]),
+ must_not: [{ exists: { field: 'namespace' } }],
+ },
+ };
+ } else if (registry.isSingleNamespace(type)) {
+ const nonDefaultNamespaces = namespaces?.filter((n) => n !== 'default') ?? [];
+ const should: any = [];
+ if (nonDefaultNamespaces.length > 0) {
+ should.push({ terms: { namespace: nonDefaultNamespaces } });
+ }
+ if (namespaces?.includes('default')) {
+ should.push({ bool: { must_not: [{ exists: { field: 'namespace' } }] } });
+ }
+ return {
+ bool: {
+ must: [{ term: { type } }],
+ should: expect.arrayContaining(should),
+ minimum_should_match: 1,
+ must_not: [{ exists: { field: 'namespaces' } }],
+ },
+ };
+ }
+ // isNamespaceAgnostic
+ return {
+ bool: expect.objectContaining({
+ must_not: [{ exists: { field: 'namespace' } }, { exists: { field: 'namespaces' } }],
+ }),
+ };
+ };
+
describe('kueryNode filter clause', () => {
const expectResult = (result: Result, expected: any) => {
expect(result.query.bool.filter).toEqual(expect.arrayContaining([expected]));
@@ -100,13 +141,13 @@ describe('#getQueryParams', () => {
describe('`kueryNode` parameter', () => {
it('does not include the clause when `kueryNode` is not specified', () => {
- const result = getQueryParams({ mappings, registry, kueryNode: undefined });
+ const result = getQueryParams({ registry, kueryNode: undefined });
expect(result.query.bool.filter).toHaveLength(1);
});
it('includes the specified Kuery clause', () => {
const test = (kueryNode: KueryNode) => {
- const result = getQueryParams({ mappings, registry, kueryNode });
+ const result = getQueryParams({ registry, kueryNode });
const expected = esKuery.toElasticsearchQuery(kueryNode);
expect(result.query.bool.filter).toHaveLength(2);
expectResult(result, expected);
@@ -165,7 +206,6 @@ describe('#getQueryParams', () => {
it('does not include the clause when `hasReference` is not specified', () => {
const result = getQueryParams({
- mappings,
registry,
hasReference: undefined,
});
@@ -176,7 +216,6 @@ describe('#getQueryParams', () => {
it('creates a should clause for specified reference when operator is `OR`', () => {
const hasReference = { id: 'foo', type: 'bar' };
const result = getQueryParams({
- mappings,
registry,
hasReference,
hasReferenceOperator: 'OR',
@@ -192,7 +231,6 @@ describe('#getQueryParams', () => {
it('creates a must clause for specified reference when operator is `AND`', () => {
const hasReference = { id: 'foo', type: 'bar' };
const result = getQueryParams({
- mappings,
registry,
hasReference,
hasReferenceOperator: 'AND',
@@ -210,7 +248,6 @@ describe('#getQueryParams', () => {
{ id: 'hello', type: 'dolly' },
];
const result = getQueryParams({
- mappings,
registry,
hasReference,
hasReferenceOperator: 'OR',
@@ -229,7 +266,6 @@ describe('#getQueryParams', () => {
{ id: 'hello', type: 'dolly' },
];
const result = getQueryParams({
- mappings,
registry,
hasReference,
hasReferenceOperator: 'AND',
@@ -244,7 +280,6 @@ describe('#getQueryParams', () => {
it('defaults to `OR` when operator is not specified', () => {
const hasReference = { id: 'foo', type: 'bar' };
const result = getQueryParams({
- mappings,
registry,
hasReference,
});
@@ -278,14 +313,13 @@ describe('#getQueryParams', () => {
};
it('searches for all known types when `type` is not specified', () => {
- const result = getQueryParams({ mappings, registry, type: undefined });
+ const result = getQueryParams({ registry, type: undefined });
expectResult(result, ...ALL_TYPES);
});
it('searches for specified type/s', () => {
const test = (typeOrTypes: string | string[]) => {
const result = getQueryParams({
- mappings,
registry,
type: typeOrTypes,
});
@@ -309,18 +343,17 @@ describe('#getQueryParams', () => {
const test = (namespaces?: string[]) => {
for (const typeOrTypes of ALL_TYPE_SUBSETS) {
- const result = getQueryParams({ mappings, registry, type: typeOrTypes, namespaces });
+ const result = getQueryParams({ registry, type: typeOrTypes, namespaces });
const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
expectResult(result, ...types.map((x) => createTypeClause(x, namespaces)));
}
// also test with no specified type/s
- const result = getQueryParams({ mappings, registry, type: undefined, namespaces });
+ const result = getQueryParams({ registry, type: undefined, namespaces });
expectResult(result, ...ALL_TYPES.map((x) => createTypeClause(x, namespaces)));
};
it('normalizes and deduplicates provided namespaces', () => {
const result = getQueryParams({
- mappings,
registry,
search: '*',
namespaces: ['foo', '*', 'foo', 'bar', 'default'],
@@ -360,7 +393,6 @@ describe('#getQueryParams', () => {
it('supersedes `type` and `namespaces` parameters', () => {
const result = getQueryParams({
- mappings,
registry,
type: ['pending', 'saved', 'shared', 'global'],
namespaces: ['foo', 'bar', 'default'],
@@ -381,148 +413,266 @@ describe('#getQueryParams', () => {
});
});
- describe('search clause (query.bool.must.simple_query_string)', () => {
- const search = 'foo*';
+ describe('search clause (query.bool)', () => {
+ describe('when using simple search (query.bool.must.simple_query_string)', () => {
+ const search = 'foo';
- const expectResult = (result: Result, sqsClause: any) => {
- expect(result.query.bool.must).toEqual([{ simple_query_string: sqsClause }]);
- };
+ const expectResult = (result: Result, sqsClause: any) => {
+ expect(result.query.bool.must).toEqual([{ simple_query_string: sqsClause }]);
+ };
- describe('`search` parameter', () => {
- it('does not include clause when `search` is not specified', () => {
- const result = getQueryParams({
- mappings,
- registry,
- search: undefined,
+ describe('`search` parameter', () => {
+ it('does not include clause when `search` is not specified', () => {
+ const result = getQueryParams({
+ registry,
+ search: undefined,
+ });
+ expect(result.query.bool.must).toBeUndefined();
});
- expect(result.query.bool.must).toBeUndefined();
- });
- it('creates a clause with query for specified search', () => {
- const result = getQueryParams({
- mappings,
- registry,
- search,
+ it('creates a clause with query for specified search', () => {
+ const result = getQueryParams({
+ registry,
+ search,
+ });
+ expectResult(result, expect.objectContaining({ query: search }));
});
- expectResult(result, expect.objectContaining({ query: search }));
});
- });
- describe('`searchFields` and `rootSearchFields` parameters', () => {
- const getExpectedFields = (searchFields: string[], typeOrTypes: string | string[]) => {
- const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
- return searchFields.map((x) => types.map((y) => `${y}.${x}`)).flat();
- };
+ describe('`searchFields` and `rootSearchFields` parameters', () => {
+ const getExpectedFields = (searchFields: string[], typeOrTypes: string | string[]) => {
+ const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
+ return searchFields.map((x) => types.map((y) => `${y}.${x}`)).flat();
+ };
- const test = ({
- searchFields,
- rootSearchFields,
- }: {
- searchFields?: string[];
- rootSearchFields?: string[];
- }) => {
- for (const typeOrTypes of ALL_TYPE_SUBSETS) {
+ const test = ({
+ searchFields,
+ rootSearchFields,
+ }: {
+ searchFields?: string[];
+ rootSearchFields?: string[];
+ }) => {
+ for (const typeOrTypes of ALL_TYPE_SUBSETS) {
+ const result = getQueryParams({
+ registry,
+ type: typeOrTypes,
+ search,
+ searchFields,
+ rootSearchFields,
+ });
+ let fields = rootSearchFields || [];
+ if (searchFields) {
+ fields = fields.concat(getExpectedFields(searchFields, typeOrTypes));
+ }
+ expectResult(result, expect.objectContaining({ fields }));
+ }
+ // also test with no specified type/s
const result = getQueryParams({
- mappings,
registry,
- type: typeOrTypes,
+ type: undefined,
search,
searchFields,
rootSearchFields,
});
let fields = rootSearchFields || [];
if (searchFields) {
- fields = fields.concat(getExpectedFields(searchFields, typeOrTypes));
+ fields = fields.concat(getExpectedFields(searchFields, ALL_TYPES));
}
expectResult(result, expect.objectContaining({ fields }));
- }
- // also test with no specified type/s
- const result = getQueryParams({
- mappings,
- registry,
- type: undefined,
- search,
- searchFields,
- rootSearchFields,
+ };
+
+ it('throws an error if a raw search field contains a "." character', () => {
+ expect(() =>
+ getQueryParams({
+ registry,
+ type: undefined,
+ search,
+ searchFields: undefined,
+ rootSearchFields: ['foo', 'bar.baz'],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"rootSearchFields entry \\"bar.baz\\" is invalid: cannot contain \\".\\" character"`
+ );
});
- let fields = rootSearchFields || [];
- if (searchFields) {
- fields = fields.concat(getExpectedFields(searchFields, ALL_TYPES));
- }
- expectResult(result, expect.objectContaining({ fields }));
- };
- it('throws an error if a raw search field contains a "." character', () => {
- expect(() =>
- getQueryParams({
- mappings,
+ it('includes lenient flag and all fields when `searchFields` and `rootSearchFields` are not specified', () => {
+ const result = getQueryParams({
registry,
- type: undefined,
search,
searchFields: undefined,
- rootSearchFields: ['foo', 'bar.baz'],
- })
- ).toThrowErrorMatchingInlineSnapshot(
- `"rootSearchFields entry \\"bar.baz\\" is invalid: cannot contain \\".\\" character"`
- );
+ rootSearchFields: undefined,
+ });
+ expectResult(result, expect.objectContaining({ lenient: true, fields: ['*'] }));
+ });
+
+ it('includes specified search fields for appropriate type/s', () => {
+ test({ searchFields: ['title'] });
+ });
+
+ it('supports boosting', () => {
+ test({ searchFields: ['title^3'] });
+ });
+
+ it('supports multiple search fields', () => {
+ test({ searchFields: ['title, title.raw'] });
+ });
+
+ it('includes specified raw search fields', () => {
+ test({ rootSearchFields: ['_id'] });
+ });
+
+ it('supports multiple raw search fields', () => {
+ test({ rootSearchFields: ['_id', 'originId'] });
+ });
+
+ it('supports search fields and raw search fields', () => {
+ test({ searchFields: ['title'], rootSearchFields: ['_id'] });
+ });
});
- it('includes lenient flag and all fields when `searchFields` and `rootSearchFields` are not specified', () => {
- const result = getQueryParams({
- mappings,
+ describe('`defaultSearchOperator` parameter', () => {
+ it('does not include default_operator when `defaultSearchOperator` is not specified', () => {
+ const result = getQueryParams({
+ registry,
+ search,
+ defaultSearchOperator: undefined,
+ });
+ expectResult(
+ result,
+ expect.not.objectContaining({ default_operator: expect.anything() })
+ );
+ });
+
+ it('includes specified default operator', () => {
+ const defaultSearchOperator = 'AND';
+ const result = getQueryParams({
+ registry,
+ search,
+ defaultSearchOperator,
+ });
+ expectResult(
+ result,
+ expect.objectContaining({ default_operator: defaultSearchOperator })
+ );
+ });
+ });
+ });
+
+ describe('when using prefix search (query.bool.should)', () => {
+ const searchQuery = 'foo*';
+
+ const getQueryParamForSearch = ({
+ search,
+ searchFields,
+ type,
+ }: {
+ search?: string;
+ searchFields?: string[];
+ type?: string[];
+ }) =>
+ getQueryParams({
registry,
search,
- searchFields: undefined,
- rootSearchFields: undefined,
+ searchFields,
+ type,
});
- expectResult(result, expect.objectContaining({ lenient: true, fields: ['*'] }));
- });
- it('includes specified search fields for appropriate type/s', () => {
- test({ searchFields: ['title'] });
- });
+ it('uses a `should` clause instead of `must`', () => {
+ const result = getQueryParamForSearch({ search: searchQuery, searchFields: ['title'] });
- it('supports boosting', () => {
- test({ searchFields: ['title^3'] });
+ expect(result.query.bool.must).toBeUndefined();
+ expect(result.query.bool.should).toEqual(expect.any(Array));
+ expect(result.query.bool.should.length).toBeGreaterThanOrEqual(1);
+ expect(result.query.bool.minimum_should_match).toBe(1);
});
-
- it('supports multiple search fields', () => {
- test({ searchFields: ['title, title.raw'] });
+ it('includes the `simple_query_string` in the `should` clauses', () => {
+ const result = getQueryParamForSearch({ search: searchQuery, searchFields: ['title'] });
+ expect(result.query.bool.should[0]).toEqual({
+ simple_query_string: expect.objectContaining({
+ query: searchQuery,
+ }),
+ });
});
- it('includes specified raw search fields', () => {
- test({ rootSearchFields: ['_id'] });
+ it('adds a should clause for each `searchFields` / `type` tuple', () => {
+ const result = getQueryParamForSearch({
+ search: searchQuery,
+ searchFields: ['title', 'desc'],
+ type: ['saved', 'pending'],
+ });
+ const shouldClauses = result.query.bool.should;
+
+ expect(shouldClauses.length).toBe(5);
+
+ const mppClauses = shouldClauses.slice(1);
+
+ expect(
+ mppClauses.map((clause: any) => Object.keys(clause.match_phrase_prefix)[0])
+ ).toEqual(['saved.title', 'pending.title', 'saved.desc', 'pending.desc']);
});
- it('supports multiple raw search fields', () => {
- test({ rootSearchFields: ['_id', 'originId'] });
+ it('uses all registered types when `type` is not provided', () => {
+ const result = getQueryParamForSearch({
+ search: searchQuery,
+ searchFields: ['title'],
+ type: undefined,
+ });
+ const shouldClauses = result.query.bool.should;
+
+ expect(shouldClauses.length).toBe(5);
+
+ const mppClauses = shouldClauses.slice(1);
+
+ expect(
+ mppClauses.map((clause: any) => Object.keys(clause.match_phrase_prefix)[0])
+ ).toEqual(['pending.title', 'saved.title', 'shared.title', 'global.title']);
});
- it('supports search fields and raw search fields', () => {
- test({ searchFields: ['title'], rootSearchFields: ['_id'] });
+ it('removes the prefix search wildcard from the query', () => {
+ const result = getQueryParamForSearch({
+ search: searchQuery,
+ searchFields: ['title'],
+ type: ['saved'],
+ });
+ const shouldClauses = result.query.bool.should;
+ const mppClauses = shouldClauses.slice(1);
+
+ expect(mppClauses[0].match_phrase_prefix['saved.title'].query).toEqual('foo');
});
- });
- describe('`defaultSearchOperator` parameter', () => {
- it('does not include default_operator when `defaultSearchOperator` is not specified', () => {
- const result = getQueryParams({
- mappings,
- registry,
- search,
- defaultSearchOperator: undefined,
+ it("defaults to the type's default search field when `searchFields` is not specified", () => {
+ const result = getQueryParamForSearch({
+ search: searchQuery,
+ searchFields: undefined,
+ type: ['saved', 'global'],
});
- expectResult(result, expect.not.objectContaining({ default_operator: expect.anything() }));
+ const shouldClauses = result.query.bool.should;
+
+ expect(shouldClauses.length).toBe(3);
+
+ const mppClauses = shouldClauses.slice(1);
+
+ expect(
+ mppClauses.map((clause: any) => Object.keys(clause.match_phrase_prefix)[0])
+ ).toEqual(['saved.title', 'global.name']);
});
- it('includes specified default operator', () => {
- const defaultSearchOperator = 'AND';
- const result = getQueryParams({
- mappings,
- registry,
- search,
- defaultSearchOperator,
+ it('supports boosting', () => {
+ const result = getQueryParamForSearch({
+ search: searchQuery,
+ searchFields: ['title^3', 'description'],
+ type: ['saved'],
});
- expectResult(result, expect.objectContaining({ default_operator: defaultSearchOperator }));
+ const shouldClauses = result.query.bool.should;
+
+ expect(shouldClauses.length).toBe(3);
+
+ const mppClauses = shouldClauses.slice(1);
+
+ expect(mppClauses.map((clause: any) => clause.match_phrase_prefix)).toEqual([
+ { 'saved.title': { query: 'foo', boost: 3 } },
+ { 'saved.description': { query: 'foo', boost: 1 } },
+ ]);
});
});
});
@@ -532,7 +682,6 @@ describe('#getQueryParams', () => {
it(`throws for ${type} when namespaces is an empty array`, () => {
expect(() =>
getQueryParams({
- mappings,
registry,
namespaces: [],
})
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts
index 8d4fe13b9bede..f73777c4f454f 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts
@@ -20,7 +20,6 @@
import { esKuery } from '../../../es_query';
type KueryNode = any;
-import { getRootPropertiesObjects, IndexMapping } from '../../../mappings';
import { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry';
import { ALL_NAMESPACES_STRING, DEFAULT_NAMESPACE_STRING } from '../utils';
@@ -28,22 +27,17 @@ import { ALL_NAMESPACES_STRING, DEFAULT_NAMESPACE_STRING } from '../utils';
* Gets the types based on the type. Uses mappings to support
* null type (all types), a single type string or an array
*/
-function getTypes(mappings: IndexMapping, type?: string | string[]) {
+function getTypes(registry: ISavedObjectTypeRegistry, type?: string | string[]) {
if (!type) {
- return Object.keys(getRootPropertiesObjects(mappings));
+ return registry.getAllTypes().map((registeredType) => registeredType.name);
}
-
- if (Array.isArray(type)) {
- return type;
- }
-
- return [type];
+ return Array.isArray(type) ? type : [type];
}
/**
* Get the field params based on the types, searchFields, and rootSearchFields
*/
-function getFieldsForTypes(
+function getSimpleQueryStringTypeFields(
types: string[],
searchFields: string[] = [],
rootSearchFields: string[] = []
@@ -130,7 +124,6 @@ export interface HasReferenceQueryParams {
export type SearchOperator = 'AND' | 'OR';
interface QueryParams {
- mappings: IndexMapping;
registry: ISavedObjectTypeRegistry;
namespaces?: string[];
type?: string | string[];
@@ -188,11 +181,26 @@ export function getClauseForReference(reference: HasReferenceQueryParams) {
};
}
+// A de-duplicated set of namespaces makes for a more efficient query.
+//
+// Additionally, we treat the `*` namespace as the `default` namespace.
+// In the Default Distribution, the `*` is automatically expanded to include all available namespaces.
+// However, the OSS distribution (and certain configurations of the Default Distribution) can allow the `*`
+// to pass through to the SO Repository, and eventually to this module. When this happens, we translate to `default`,
+// since that is consistent with how a single-namespace search behaves in the OSS distribution. Leaving the wildcard in place
+// would result in no results being returned, as the wildcard is treated as a literal, and not _actually_ as a wildcard.
+// We had a good discussion around the tradeoffs here: https://github.com/elastic/kibana/pull/67644#discussion_r441055716
+const normalizeNamespaces = (namespacesToNormalize?: string[]) =>
+ namespacesToNormalize
+ ? Array.from(
+ new Set(namespacesToNormalize.map((x) => (x === '*' ? DEFAULT_NAMESPACE_STRING : x)))
+ )
+ : undefined;
+
/**
* Get the "query" related keys for the search body
*/
export function getQueryParams({
- mappings,
registry,
namespaces,
type,
@@ -206,7 +214,7 @@ export function getQueryParams({
kueryNode,
}: QueryParams) {
const types = getTypes(
- mappings,
+ registry,
typeToNamespacesMap ? Array.from(typeToNamespacesMap.keys()) : type
);
@@ -214,28 +222,10 @@ export function getQueryParams({
hasReference = [hasReference];
}
- // A de-duplicated set of namespaces makes for a more effecient query.
- //
- // Additonally, we treat the `*` namespace as the `default` namespace.
- // In the Default Distribution, the `*` is automatically expanded to include all available namespaces.
- // However, the OSS distribution (and certain configurations of the Default Distribution) can allow the `*`
- // to pass through to the SO Repository, and eventually to this module. When this happens, we translate to `default`,
- // since that is consistent with how a single-namespace search behaves in the OSS distribution. Leaving the wildcard in place
- // would result in no results being returned, as the wildcard is treated as a literal, and not _actually_ as a wildcard.
- // We had a good discussion around the tradeoffs here: https://github.com/elastic/kibana/pull/67644#discussion_r441055716
- const normalizeNamespaces = (namespacesToNormalize?: string[]) =>
- namespacesToNormalize
- ? Array.from(
- new Set(namespacesToNormalize.map((x) => (x === '*' ? DEFAULT_NAMESPACE_STRING : x)))
- )
- : undefined;
-
const bool: any = {
filter: [
...(kueryNode != null ? [esKuery.toElasticsearchQuery(kueryNode)] : []),
- ...(hasReference && hasReference.length
- ? [getReferencesFilter(hasReference, hasReferenceOperator)]
- : []),
+ ...(hasReference?.length ? [getReferencesFilter(hasReference, hasReferenceOperator)] : []),
{
bool: {
should: types.map((shouldType) => {
@@ -251,16 +241,133 @@ export function getQueryParams({
};
if (search) {
- bool.must = [
- {
- simple_query_string: {
- query: search,
- ...getFieldsForTypes(types, searchFields, rootSearchFields),
- ...(defaultSearchOperator ? { default_operator: defaultSearchOperator } : {}),
- },
- },
- ];
+ const useMatchPhrasePrefix = shouldUseMatchPhrasePrefix(search);
+ const simpleQueryStringClause = getSimpleQueryStringClause({
+ search,
+ types,
+ searchFields,
+ rootSearchFields,
+ defaultSearchOperator,
+ });
+
+ if (useMatchPhrasePrefix) {
+ bool.should = [
+ simpleQueryStringClause,
+ ...getMatchPhrasePrefixClauses({ search, searchFields, types, registry }),
+ ];
+ bool.minimum_should_match = 1;
+ } else {
+ bool.must = [simpleQueryStringClause];
+ }
}
return { query: { bool } };
}
+
+// we only want to add match_phrase_prefix clauses
+// if the search is a prefix search
+const shouldUseMatchPhrasePrefix = (search: string): boolean => {
+ return search.trim().endsWith('*');
+};
+
+const getMatchPhrasePrefixClauses = ({
+ search,
+ searchFields,
+ registry,
+ types,
+}: {
+ search: string;
+ searchFields?: string[];
+ types: string[];
+ registry: ISavedObjectTypeRegistry;
+}) => {
+ // need to remove the prefix search operator
+ const query = search.replace(/[*]$/, '');
+ const mppFields = getMatchPhrasePrefixFields({ searchFields, types, registry });
+ return mppFields.map(({ field, boost }) => {
+ return {
+ match_phrase_prefix: {
+ [field]: {
+ query,
+ boost,
+ },
+ },
+ };
+ });
+};
+
+interface FieldWithBoost {
+ field: string;
+ boost?: number;
+}
+
+const getMatchPhrasePrefixFields = ({
+ searchFields = [],
+ types,
+ registry,
+}: {
+ searchFields?: string[];
+ types: string[];
+ registry: ISavedObjectTypeRegistry;
+}): FieldWithBoost[] => {
+ const output: FieldWithBoost[] = [];
+
+ searchFields = searchFields.filter((field) => field !== '*');
+ let fields: string[];
+ if (searchFields.length === 0) {
+ fields = types.reduce((typeFields, type) => {
+ const defaultSearchField = registry.getType(type)?.management?.defaultSearchField;
+ if (defaultSearchField) {
+ return [...typeFields, `${type}.${defaultSearchField}`];
+ }
+ return typeFields;
+ }, [] as string[]);
+ } else {
+ fields = [];
+ for (const field of searchFields) {
+ fields = fields.concat(types.map((type) => `${type}.${field}`));
+ }
+ }
+
+ fields.forEach((rawField) => {
+ const [field, rawBoost] = rawField.split('^');
+ let boost: number = 1;
+ if (rawBoost) {
+ try {
+ boost = parseInt(rawBoost, 10);
+ } catch (e) {
+ boost = 1;
+ }
+ }
+ if (isNaN(boost)) {
+ boost = 1;
+ }
+ output.push({
+ field,
+ boost,
+ });
+ });
+ return output;
+};
+
+const getSimpleQueryStringClause = ({
+ search,
+ types,
+ searchFields,
+ rootSearchFields,
+ defaultSearchOperator,
+}: {
+ search: string;
+ types: string[];
+ searchFields?: string[];
+ rootSearchFields?: string[];
+ defaultSearchOperator?: SearchOperator;
+}) => {
+ return {
+ simple_query_string: {
+ query: search,
+ ...getSimpleQueryStringTypeFields(types, searchFields, rootSearchFields),
+ ...(defaultSearchOperator ? { default_operator: defaultSearchOperator } : {}),
+ },
+ };
+};
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts
index a9f26f71a3f2b..3522ab9ef1736 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts
@@ -76,7 +76,6 @@ describe('getSearchDsl', () => {
getSearchDsl(mappings, registry, opts);
expect(getQueryParams).toHaveBeenCalledTimes(1);
expect(getQueryParams).toHaveBeenCalledWith({
- mappings,
registry,
namespaces: opts.namespaces,
type: opts.type,
diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts
index d5da82e5617be..bddecc4d7f649 100644
--- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts
+++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts
@@ -71,7 +71,6 @@ export function getSearchDsl(
return {
...getQueryParams({
- mappings,
registry,
namespaces,
type,
diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js
index c2e36b4a669ff..e5da46644672b 100644
--- a/test/api_integration/apis/saved_objects/find.js
+++ b/test/api_integration/apis/saved_objects/find.js
@@ -334,6 +334,70 @@ export default function ({ getService }) {
});
});
+ describe('searching for special characters', () => {
+ before(() => esArchiver.load('saved_objects/find_edgecases'));
+ after(() => esArchiver.unload('saved_objects/find_edgecases'));
+
+ it('can search for objects with dashes', async () =>
+ await supertest
+ .get('/api/saved_objects/_find')
+ .query({
+ type: 'visualization',
+ search_fields: 'title',
+ search: 'my-vis*',
+ })
+ .expect(200)
+ .then((resp) => {
+ const savedObjects = resp.body.saved_objects;
+ expect(savedObjects.map((so) => so.attributes.title)).to.eql(['my-visualization']);
+ }));
+
+ it('can search with the prefix search character just after a special one', async () =>
+ await supertest
+ .get('/api/saved_objects/_find')
+ .query({
+ type: 'visualization',
+ search_fields: 'title',
+ search: 'my-*',
+ })
+ .expect(200)
+ .then((resp) => {
+ const savedObjects = resp.body.saved_objects;
+ expect(savedObjects.map((so) => so.attributes.title)).to.eql(['my-visualization']);
+ }));
+
+ it('can search for objects with asterisk', async () =>
+ await supertest
+ .get('/api/saved_objects/_find')
+ .query({
+ type: 'visualization',
+ search_fields: 'title',
+ search: 'some*vi*',
+ })
+ .expect(200)
+ .then((resp) => {
+ const savedObjects = resp.body.saved_objects;
+ expect(savedObjects.map((so) => so.attributes.title)).to.eql(['some*visualization']);
+ }));
+
+ it('can still search tokens by prefix', async () =>
+ await supertest
+ .get('/api/saved_objects/_find')
+ .query({
+ type: 'visualization',
+ search_fields: 'title',
+ search: 'visuali*',
+ })
+ .expect(200)
+ .then((resp) => {
+ const savedObjects = resp.body.saved_objects;
+ expect(savedObjects.map((so) => so.attributes.title)).to.eql([
+ 'my-visualization',
+ 'some*visualization',
+ ]);
+ }));
+ });
+
describe('without kibana index', () => {
before(
async () =>
diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/data.json b/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/data.json
new file mode 100644
index 0000000000000..0c8b35fd3f499
--- /dev/null
+++ b/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/data.json
@@ -0,0 +1,93 @@
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "visualization:title-with-dash",
+ "source": {
+ "type": "visualization",
+ "updated_at": "2017-09-21T18:51:23.794Z",
+ "visualization": {
+ "title": "my-visualization",
+ "visState": "{}",
+ "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}",
+ "description": "",
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ }
+ },
+ "references": []
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "visualization:title-with-asterisk",
+ "source": {
+ "type": "visualization",
+ "updated_at": "2017-09-21T18:51:23.794Z",
+ "visualization": {
+ "title": "some*visualization",
+ "visState": "{}",
+ "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}",
+ "description": "",
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ }
+ },
+ "references": []
+ }
+ }
+}
+
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "visualization:noise-1",
+ "source": {
+ "type": "visualization",
+ "updated_at": "2017-09-21T18:51:23.794Z",
+ "visualization": {
+ "title": "Just some noise in the dataset",
+ "visState": "{}",
+ "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}",
+ "description": "",
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ }
+ },
+ "references": []
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "visualization:noise-2",
+ "source": {
+ "type": "visualization",
+ "updated_at": "2017-09-21T18:51:23.794Z",
+ "visualization": {
+ "title": "Just some noise in the dataset",
+ "visState": "{}",
+ "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}",
+ "description": "",
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ }
+ },
+ "references": []
+ }
+ }
+}
+
diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/mappings.json b/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/mappings.json
new file mode 100644
index 0000000000000..e601c43431437
--- /dev/null
+++ b/test/api_integration/fixtures/es_archiver/saved_objects/find_edgecases/mappings.json
@@ -0,0 +1,267 @@
+{
+ "type": "index",
+ "value": {
+ "index": ".kibana",
+ "settings": {
+ "index": {
+ "number_of_shards": "1",
+ "number_of_replicas": "1"
+ }
+ },
+ "mappings": {
+ "dynamic": "strict",
+ "properties": {
+ "config": {
+ "dynamic": "true",
+ "properties": {
+ "buildNum": {
+ "type": "keyword"
+ },
+ "defaultIndex": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ }
+ }
+ },
+ "dashboard": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "optionsJSON": {
+ "type": "text"
+ },
+ "panelsJSON": {
+ "type": "text"
+ },
+ "refreshInterval": {
+ "properties": {
+ "display": {
+ "type": "keyword"
+ },
+ "pause": {
+ "type": "boolean"
+ },
+ "section": {
+ "type": "integer"
+ },
+ "value": {
+ "type": "integer"
+ }
+ }
+ },
+ "timeFrom": {
+ "type": "keyword"
+ },
+ "timeRestore": {
+ "type": "boolean"
+ },
+ "timeTo": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "index-pattern": {
+ "properties": {
+ "fieldFormatMap": {
+ "type": "text"
+ },
+ "fields": {
+ "type": "text"
+ },
+ "intervalName": {
+ "type": "keyword"
+ },
+ "notExpandable": {
+ "type": "boolean"
+ },
+ "sourceFilters": {
+ "type": "text"
+ },
+ "timeFieldName": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ }
+ }
+ },
+ "search": {
+ "properties": {
+ "columns": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "sort": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "server": {
+ "properties": {
+ "uuid": {
+ "type": "keyword"
+ }
+ }
+ },
+ "timelion-sheet": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "timelion_chart_height": {
+ "type": "integer"
+ },
+ "timelion_columns": {
+ "type": "integer"
+ },
+ "timelion_interval": {
+ "type": "keyword"
+ },
+ "timelion_other_interval": {
+ "type": "keyword"
+ },
+ "timelion_rows": {
+ "type": "integer"
+ },
+ "timelion_sheet": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "namespace": {
+ "type": "keyword"
+ },
+ "references": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "url": {
+ "properties": {
+ "accessCount": {
+ "type": "long"
+ },
+ "accessDate": {
+ "type": "date"
+ },
+ "createDate": {
+ "type": "date"
+ },
+ "url": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 2048
+ }
+ }
+ }
+ }
+ },
+ "visualization": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "savedSearchId": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ },
+ "visState": {
+ "type": "text"
+ }
+ }
+ }
+ }
+ }
+ }
+}
From 202dec7c24d14ae2e20be97743089e377daf3047 Mon Sep 17 00:00:00 2001
From: Alexey Antonov
Date: Mon, 9 Nov 2020 14:17:29 +0300
Subject: [PATCH 3/9] Enable send to background in TSVB (#82835)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
src/plugins/vis_type_timeseries/common/types.ts | 3 ++-
.../vis_type_timeseries/common/vis_schema.ts | 1 +
.../vis_type_timeseries/public/request_handler.js | 4 +++-
.../strategies/abstract_search_strategy.test.js | 8 +++++++-
.../strategies/abstract_search_strategy.ts | 13 +++++++++----
5 files changed, 22 insertions(+), 7 deletions(-)
diff --git a/src/plugins/vis_type_timeseries/common/types.ts b/src/plugins/vis_type_timeseries/common/types.ts
index 4520069244527..8973060848b41 100644
--- a/src/plugins/vis_type_timeseries/common/types.ts
+++ b/src/plugins/vis_type_timeseries/common/types.ts
@@ -18,8 +18,9 @@
*/
import { TypeOf } from '@kbn/config-schema';
-import { metricsItems, panel, seriesItems } from './vis_schema';
+import { metricsItems, panel, seriesItems, visPayloadSchema } from './vis_schema';
export type SeriesItemsSchema = TypeOf;
export type MetricsItemsSchema = TypeOf;
export type PanelSchema = TypeOf;
+export type VisPayload = TypeOf;
diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts
index 40f776050617e..27f09fb574b0f 100644
--- a/src/plugins/vis_type_timeseries/common/vis_schema.ts
+++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts
@@ -273,4 +273,5 @@ export const visPayloadSchema = schema.object({
min: stringRequired,
max: stringRequired,
}),
+ sessionId: schema.maybe(schema.string()),
});
diff --git a/src/plugins/vis_type_timeseries/public/request_handler.js b/src/plugins/vis_type_timeseries/public/request_handler.js
index e33d0e254f609..12b7f3d417ef6 100644
--- a/src/plugins/vis_type_timeseries/public/request_handler.js
+++ b/src/plugins/vis_type_timeseries/public/request_handler.js
@@ -32,7 +32,8 @@ export const metricsRequestHandler = async ({
const config = getUISettings();
const timezone = getTimezone(config);
const uiStateObj = uiState.get(visParams.type, {});
- const parsedTimeRange = getDataStart().query.timefilter.timefilter.calculateBounds(timeRange);
+ const dataSearch = getDataStart();
+ const parsedTimeRange = dataSearch.query.timefilter.timefilter.calculateBounds(timeRange);
const scaledDataFormat = config.get('dateFormat:scaled');
const dateFormat = config.get('dateFormat');
@@ -53,6 +54,7 @@ export const metricsRequestHandler = async ({
panels: [visParams],
state: uiStateObj,
savedObjectId: savedObjectId || 'unsaved',
+ sessionId: dataSearch.search.session.getSessionId(),
}),
});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js
index 9710f7daf69b6..2c38e883cd69f 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js
@@ -28,6 +28,7 @@ describe('AbstractSearchStrategy', () => {
beforeEach(() => {
mockedFields = {};
req = {
+ payload: {},
pre: {
indexPatternsService: {
getFieldsForWildcard: jest.fn().mockReturnValue(mockedFields),
@@ -60,6 +61,9 @@ describe('AbstractSearchStrategy', () => {
const responses = await abstractSearchStrategy.search(
{
+ payload: {
+ sessionId: 1,
+ },
requestContext: {
search: { search: searchFn },
},
@@ -76,7 +80,9 @@ describe('AbstractSearchStrategy', () => {
},
indexType: undefined,
},
- {}
+ {
+ sessionId: 1,
+ }
);
});
});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts
index eb22fcb1dd689..b1e21edf8b588 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts
@@ -23,8 +23,10 @@ import {
IUiSettingsClient,
SavedObjectsClientContract,
} from 'kibana/server';
+
import { Framework } from '../../../plugin';
import { IndexPatternsFetcher } from '../../../../../data/server';
+import { VisPayload } from '../../../../common/types';
/**
* ReqFacade is a regular KibanaRequest object extended with additional service
@@ -32,17 +34,17 @@ import { IndexPatternsFetcher } from '../../../../../data/server';
*
* This will be replaced by standard KibanaRequest and RequestContext objects in a later version.
*/
-export type ReqFacade = FakeRequest & {
+export interface ReqFacade extends FakeRequest {
requestContext: RequestHandlerContext;
framework: Framework;
- payload: unknown;
+ payload: T;
pre: {
indexPatternsService?: IndexPatternsFetcher;
};
getUiSettingsService: () => IUiSettingsClient;
getSavedObjectsClient: () => SavedObjectsClientContract;
getEsShardTimeout: () => Promise;
-};
+}
export class AbstractSearchStrategy {
public indexType?: string;
@@ -53,8 +55,10 @@ export class AbstractSearchStrategy {
this.additionalParams = additionalParams;
}
- async search(req: ReqFacade, bodies: any[], options = {}) {
+ async search(req: ReqFacade, bodies: any[], options = {}) {
const requests: any[] = [];
+ const { sessionId } = req.payload;
+
bodies.forEach((body) => {
requests.push(
req.requestContext
@@ -67,6 +71,7 @@ export class AbstractSearchStrategy {
indexType: this.indexType,
},
{
+ sessionId,
...options,
}
)
From 6110ef82a3a0537a82738c3f039d850772f24306 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?=
Date: Mon, 9 Nov 2020 12:43:11 +0100
Subject: [PATCH 4/9] [Logs UI] Fix errors during navigation (#78319)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../containers/logs/log_entries/index.ts | 24 ++++++++----
.../infra/public/utils/use_tracked_promise.ts | 37 ++++++++++++++++---
2 files changed, 47 insertions(+), 14 deletions(-)
diff --git a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts
index 4c8c610794b2e..214bb16b24283 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect, useState, useReducer, useCallback } from 'react';
+import { useMountedState } from 'react-use';
import createContainer from 'constate';
import { pick, throttle } from 'lodash';
import { TimeKey, timeKeyIsBetween } from '../../../../common/time';
@@ -146,15 +147,20 @@ const useFetchEntriesEffect = (
props: LogEntriesProps
) => {
const { services } = useKibanaContextForPlugin();
+ const isMounted = useMountedState();
const [prevParams, cachePrevParams] = useState();
const [startedStreaming, setStartedStreaming] = useState(false);
+ const dispatchIfMounted = useCallback((action) => (isMounted() ? dispatch(action) : undefined), [
+ dispatch,
+ isMounted,
+ ]);
const runFetchNewEntriesRequest = async (overrides: Partial = {}) => {
if (!props.startTimestamp || !props.endTimestamp) {
return;
}
- dispatch({ type: Action.FetchingNewEntries });
+ dispatchIfMounted({ type: Action.FetchingNewEntries });
try {
const commonFetchArgs: LogEntriesBaseRequest = {
@@ -175,13 +181,15 @@ const useFetchEntriesEffect = (
};
const { data: payload } = await fetchLogEntries(fetchArgs, services.http.fetch);
- dispatch({ type: Action.ReceiveNewEntries, payload });
+ dispatchIfMounted({ type: Action.ReceiveNewEntries, payload });
// Move position to the bottom if it's the first load.
// Do it in the next tick to allow the `dispatch` to fire
if (!props.timeKey && payload.bottomCursor) {
setTimeout(() => {
- props.jumpToTargetPosition(payload.bottomCursor!);
+ if (isMounted()) {
+ props.jumpToTargetPosition(payload.bottomCursor!);
+ }
});
} else if (
props.timeKey &&
@@ -192,7 +200,7 @@ const useFetchEntriesEffect = (
props.jumpToTargetPosition(payload.topCursor);
}
} catch (e) {
- dispatch({ type: Action.ErrorOnNewEntries });
+ dispatchIfMounted({ type: Action.ErrorOnNewEntries });
}
};
@@ -210,7 +218,7 @@ const useFetchEntriesEffect = (
return;
}
- dispatch({ type: Action.FetchingMoreEntries });
+ dispatchIfMounted({ type: Action.FetchingMoreEntries });
try {
const commonFetchArgs: LogEntriesBaseRequest = {
@@ -232,14 +240,14 @@ const useFetchEntriesEffect = (
const { data: payload } = await fetchLogEntries(fetchArgs, services.http.fetch);
- dispatch({
+ dispatchIfMounted({
type: getEntriesBefore ? Action.ReceiveEntriesBefore : Action.ReceiveEntriesAfter,
payload,
});
return payload.bottomCursor;
} catch (e) {
- dispatch({ type: Action.ErrorOnMoreEntries });
+ dispatchIfMounted({ type: Action.ErrorOnMoreEntries });
}
};
@@ -322,7 +330,7 @@ const useFetchEntriesEffect = (
after: props.endTimestamp > prevParams.endTimestamp,
};
- dispatch({ type: Action.ExpandRange, payload: shouldExpand });
+ dispatchIfMounted({ type: Action.ExpandRange, payload: shouldExpand });
};
const expandRangeEffectDependencies = [
diff --git a/x-pack/plugins/infra/public/utils/use_tracked_promise.ts b/x-pack/plugins/infra/public/utils/use_tracked_promise.ts
index 9951b62fa64a3..42518127f68bf 100644
--- a/x-pack/plugins/infra/public/utils/use_tracked_promise.ts
+++ b/x-pack/plugins/infra/public/utils/use_tracked_promise.ts
@@ -6,13 +6,15 @@
/* eslint-disable max-classes-per-file */
-import { DependencyList, useEffect, useMemo, useRef, useState } from 'react';
+import { DependencyList, useEffect, useMemo, useRef, useState, useCallback } from 'react';
+import { useMountedState } from 'react-use';
interface UseTrackedPromiseArgs {
createPromise: (...args: Arguments) => Promise;
onResolve?: (result: Result) => void;
onReject?: (value: unknown) => void;
cancelPreviousOn?: 'creation' | 'settlement' | 'resolution' | 'rejection' | 'never';
+ triggerOrThrow?: 'always' | 'whenMounted';
}
/**
@@ -64,6 +66,16 @@ interface UseTrackedPromiseArgs {
* The last argument is a normal React hook dependency list that indicates
* under which conditions a new reference to the configuration object should be
* used.
+ *
+ * The `onResolve`, `onReject` and possible uncatched errors are only triggered
+ * if the underlying component is mounted. To ensure they always trigger (i.e.
+ * if the promise is called in a `useLayoutEffect`) use the `triggerOrThrow`
+ * attribute:
+ *
+ * 'whenMounted': (default) they are called only if the component is mounted.
+ *
+ * 'always': they always call. The consumer is then responsible of ensuring no
+ * side effects happen if the underlying component is not mounted.
*/
export const useTrackedPromise = (
{
@@ -71,9 +83,20 @@ export const useTrackedPromise = (
onResolve = noOp,
onReject = noOp,
cancelPreviousOn = 'never',
+ triggerOrThrow = 'whenMounted',
}: UseTrackedPromiseArgs,
dependencies: DependencyList
) => {
+ const isComponentMounted = useMountedState();
+ const shouldTriggerOrThrow = useCallback(() => {
+ switch (triggerOrThrow) {
+ case 'always':
+ return true;
+ case 'whenMounted':
+ return isComponentMounted();
+ }
+ }, [isComponentMounted, triggerOrThrow]);
+
/**
* If a promise is currently pending, this holds a reference to it and its
* cancellation function.
@@ -144,7 +167,7 @@ export const useTrackedPromise = (
(pendingPromise) => pendingPromise.promise !== newPendingPromise.promise
);
- if (onResolve) {
+ if (onResolve && shouldTriggerOrThrow()) {
onResolve(value);
}
@@ -173,11 +196,13 @@ export const useTrackedPromise = (
(pendingPromise) => pendingPromise.promise !== newPendingPromise.promise
);
- if (onReject) {
- onReject(value);
- }
+ if (shouldTriggerOrThrow()) {
+ if (onReject) {
+ onReject(value);
+ }
- throw value;
+ throw value;
+ }
}
),
};
From 858befef44d10db1ad388b10e48cad84991c8355 Mon Sep 17 00:00:00 2001
From: Dario Gieselaar
Date: Mon, 9 Nov 2020 13:46:46 +0100
Subject: [PATCH 5/9] [APM] Expose APM event client as part of plugin contract
(#82724)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../create_apm_event_client/index.ts | 21 +++++----
.../apm/server/lib/helpers/setup_request.ts | 3 +-
x-pack/plugins/apm/server/plugin.ts | 47 +++++++++++++++++--
3 files changed, 57 insertions(+), 14 deletions(-)
diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts
index 2bfd3c94ed34c..9020cb1b9953a 100644
--- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts
@@ -7,14 +7,16 @@
import { ValuesType } from 'utility-types';
import { APMBaseDoc } from '../../../../../typings/es_schemas/raw/apm_base_doc';
import { APMError } from '../../../../../typings/es_schemas/ui/apm_error';
-import { KibanaRequest } from '../../../../../../../../src/core/server';
+import {
+ KibanaRequest,
+ LegacyScopedClusterClient,
+} from '../../../../../../../../src/core/server';
import { ProcessorEvent } from '../../../../../common/processor_event';
import {
ESSearchRequest,
ESSearchResponse,
} from '../../../../../typings/elasticsearch';
import { ApmIndicesConfig } from '../../../settings/apm_indices/get_apm_indices';
-import { APMRequestHandlerContext } from '../../../../routes/typings';
import { addFilterToExcludeLegacyData } from './add_filter_to_exclude_legacy_data';
import { callClientWithDebug } from '../call_client_with_debug';
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
@@ -51,20 +53,23 @@ type TypedSearchResponse<
export type APMEventClient = ReturnType;
export function createApmEventClient({
- context,
+ esClient,
+ debug,
request,
indices,
options: { includeFrozen } = { includeFrozen: false },
}: {
- context: APMRequestHandlerContext;
+ esClient: Pick<
+ LegacyScopedClusterClient,
+ 'callAsInternalUser' | 'callAsCurrentUser'
+ >;
+ debug: boolean;
request: KibanaRequest;
indices: ApmIndicesConfig;
options: {
includeFrozen: boolean;
};
}) {
- const client = context.core.elasticsearch.legacy.client;
-
return {
search(
params: TParams,
@@ -77,14 +82,14 @@ export function createApmEventClient({
: withProcessorEventFilter;
return callClientWithDebug({
- apiCaller: client.callAsCurrentUser,
+ apiCaller: esClient.callAsCurrentUser,
operationName: 'search',
params: {
...withPossibleLegacyDataFilter,
ignore_throttled: !includeFrozen,
},
request,
- debug: context.params.query._debug,
+ debug,
});
},
};
diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
index 5e75535c678b3..363c4128137e0 100644
--- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
@@ -88,7 +88,8 @@ export async function setupRequest(
const coreSetupRequest = {
indices,
apmEventClient: createApmEventClient({
- context,
+ esClient: context.core.elasticsearch.legacy.client,
+ debug: context.params.query._debug,
request,
indices,
options: { includeFrozen },
diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts
index d3341b6c1b163..44269b1775953 100644
--- a/x-pack/plugins/apm/server/plugin.ts
+++ b/x-pack/plugins/apm/server/plugin.ts
@@ -10,14 +10,17 @@ import { map, take } from 'rxjs/operators';
import {
CoreSetup,
CoreStart,
+ KibanaRequest,
Logger,
Plugin,
PluginInitializerContext,
+ RequestHandlerContext,
} from 'src/core/server';
import { APMConfig, APMXPackConfig, mergeConfigs } from '.';
import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server';
import { HomeServerPluginSetup } from '../../../../src/plugins/home/server';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
+import { UI_SETTINGS } from '../../../../src/plugins/data/common';
import { ActionsPlugin } from '../../actions/server';
import { AlertingPlugin } from '../../alerts/server';
import { CloudSetup } from '../../cloud/server';
@@ -30,6 +33,7 @@ import { TaskManagerSetupContract } from '../../task_manager/server';
import { APM_FEATURE, registerFeaturesUsage } from './feature';
import { registerApmAlerts } from './lib/alerts/register_apm_alerts';
import { createApmTelemetry } from './lib/apm_telemetry';
+import { createApmEventClient } from './lib/helpers/create_es_client/create_apm_event_client';
import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_objects_client';
import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index';
import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices';
@@ -42,6 +46,11 @@ import { uiSettings } from './ui_settings';
export interface APMPluginSetup {
config$: Observable;
getApmIndices: () => ReturnType;
+ createApmEventClient: (params: {
+ debug?: boolean;
+ request: KibanaRequest;
+ context: RequestHandlerContext;
+ }) => Promise>;
}
export class APMPlugin implements Plugin {
@@ -141,13 +150,41 @@ export class APMPlugin implements Plugin {
},
});
+ const boundGetApmIndices = async () =>
+ getApmIndices({
+ savedObjectsClient: await getInternalSavedObjectsClient(core),
+ config: await mergedConfig$.pipe(take(1)).toPromise(),
+ });
+
return {
config$: mergedConfig$,
- getApmIndices: async () =>
- getApmIndices({
- savedObjectsClient: await getInternalSavedObjectsClient(core),
- config: await mergedConfig$.pipe(take(1)).toPromise(),
- }),
+ getApmIndices: boundGetApmIndices,
+ createApmEventClient: async ({
+ request,
+ context,
+ debug,
+ }: {
+ debug?: boolean;
+ request: KibanaRequest;
+ context: RequestHandlerContext;
+ }) => {
+ const [indices, includeFrozen] = await Promise.all([
+ boundGetApmIndices(),
+ context.core.uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN),
+ ]);
+
+ const esClient = context.core.elasticsearch.legacy.client;
+
+ return createApmEventClient({
+ debug: debug ?? false,
+ esClient,
+ request,
+ indices,
+ options: {
+ includeFrozen,
+ },
+ });
+ },
};
}
From 3c525d7341ebe683e6ed8827927c5b0c18e97631 Mon Sep 17 00:00:00 2001
From: Gidi Meir Morris
Date: Mon, 9 Nov 2020 12:56:56 +0000
Subject: [PATCH 6/9] [Alerting] adds an Run When field in the alert flyout to
assign the action to an Action Group (#82472)
Adds a `RunsWhen` field to actions in the Alerts Flyout when creating / editing an Alert which allows the user to assign specific actions to a certain Action Groups
---
.../public/alert_types/astros.tsx | 6 +-
.../server/alert_types/always_firing.ts | 18 +-
x-pack/plugins/triggers_actions_ui/README.md | 21 +-
.../lib/check_action_type_enabled.scss | 12 +-
.../action_form.test.tsx | 89 ++-
.../action_connector_form/action_form.tsx | 637 +++++-------------
.../action_type_form.tsx | 339 ++++++++++
.../connector_add_inline.tsx | 153 +++++
.../connector_add_modal.test.tsx | 3 +-
.../connector_add_modal.tsx | 13 +-
.../sections/alert_form/alert_form.tsx | 51 +-
.../alert_create_flyout.ts | 57 ++
.../fixtures/plugins/alerts/server/plugin.ts | 1 +
13 files changed, 846 insertions(+), 554 deletions(-)
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx
diff --git a/x-pack/examples/alerting_example/public/alert_types/astros.tsx b/x-pack/examples/alerting_example/public/alert_types/astros.tsx
index 73c7dfea1263b..54f989b93e22f 100644
--- a/x-pack/examples/alerting_example/public/alert_types/astros.tsx
+++ b/x-pack/examples/alerting_example/public/alert_types/astros.tsx
@@ -127,9 +127,9 @@ export const PeopleinSpaceExpression: React.FunctionComponent
- errs.map((e) => (
-
+ Object.entries(errors).map(([field, errs]: [string, string[]], fieldIndex) =>
+ errs.map((e, index) => (
+
{field}:`: ${errs}`
))
diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts
index bb1cb0d97689b..d02406a23045e 100644
--- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts
+++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts
@@ -5,25 +5,31 @@
*/
import uuid from 'uuid';
-import { range } from 'lodash';
+import { range, random } from 'lodash';
import { AlertType } from '../../../../plugins/alerts/server';
import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
+const ACTION_GROUPS = [
+ { id: 'small', name: 'small' },
+ { id: 'medium', name: 'medium' },
+ { id: 'large', name: 'large' },
+];
+
export const alertType: AlertType = {
id: 'example.always-firing',
name: 'Always firing',
- actionGroups: [{ id: 'default', name: 'default' }],
- defaultActionGroupId: 'default',
+ actionGroups: ACTION_GROUPS,
+ defaultActionGroupId: 'small',
async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) {
const count = (state.count ?? 0) + 1;
range(instances)
- .map(() => ({ id: uuid.v4() }))
- .forEach((instance: { id: string }) => {
+ .map(() => ({ id: uuid.v4(), tshirtSize: ACTION_GROUPS[random(0, 2)].id! }))
+ .forEach((instance: { id: string; tshirtSize: string }) => {
services
.alertInstanceFactory(instance.id)
.replaceState({ triggerdOnCycle: count })
- .scheduleActions('default');
+ .scheduleActions(instance.tshirtSize);
});
return {
diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md
index aabb9899cb343..32e157255c0cc 100644
--- a/x-pack/plugins/triggers_actions_ui/README.md
+++ b/x-pack/plugins/triggers_actions_ui/README.md
@@ -1319,19 +1319,19 @@ ActionForm Props definition:
interface ActionAccordionFormProps {
actions: AlertAction[];
defaultActionGroupId: string;
+ actionGroups?: ActionGroup[];
setActionIdByIndex: (id: string, index: number) => void;
+ setActionGroupIdByIndex?: (group: string, index: number) => void;
setAlertProperty: (actions: AlertAction[]) => void;
setActionParamsProperty: (key: string, value: any, index: number) => void;
http: HttpSetup;
- actionTypeRegistry: TypeRegistry;
- toastNotifications: Pick<
- ToastsApi,
- 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
- >;
+ actionTypeRegistry: ActionTypeRegistryContract;
+ toastNotifications: ToastsSetup;
+ docLinks: DocLinksStart;
actionTypes?: ActionType[];
messageVariables?: ActionVariable[];
defaultActionMessage?: string;
- consumer: string;
+ capabilities: ApplicationStart['capabilities'];
}
```
@@ -1339,17 +1339,20 @@ interface ActionAccordionFormProps {
|Property|Description|
|---|---|
|actions|List of actions comes from alert.actions property.|
-|defaultActionGroupId|Default action group id to which each new action will belong to.|
+|defaultActionGroupId|Default action group id to which each new action will belong by default.|
+|actionGroups|Optional. List of action groups to which new action can be assigned. The RunWhen field is only displayed when these action groups are specified|
|setActionIdByIndex|Function for changing action 'id' by the proper index in alert.actions array.|
+|setActionGroupIdByIndex|Function for changing action 'group' by the proper index in alert.actions array.|
|setAlertProperty|Function for changing alert property 'actions'. Used when deleting action from the array to reset it.|
|setActionParamsProperty|Function for changing action key/value property by index in alert.actions array.|
|http|HttpSetup needed for executing API calls.|
|actionTypeRegistry|Registry for action types.|
-|toastNotifications|Toast messages.|
+|toastNotifications|Toast messages Plugin Setup Contract.|
+|docLinks|Documentation links Plugin Start Contract.|
|actionTypes|Optional property, which allowes to define a list of available actions specific for a current plugin.|
|actionTypes|Optional property, which allowes to define a list of variables for action 'message' property.|
|defaultActionMessage|Optional property, which allowes to define a message value for action with 'message' property.|
-|consumer|Name of the plugin that creates an action.|
+|capabilities|Kibana core's Capabilities ApplicationStart['capabilities'].|
AlertsContextProvider value options:
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss
index 24dbb865742d8..bb622829e997a 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss
@@ -3,9 +3,15 @@
}
.actAccordionActionForm {
- .euiCard {
- box-shadow: none;
- }
+ background-color: $euiColorLightestShade;
+}
+
+.actAccordionActionForm .euiCard {
+ box-shadow: none;
+}
+
+.actAccordionActionForm__button {
+ padding: $euiSizeM;
}
.actConnectorsListGrid {
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx
index 7c718e8248e41..94452e70e6bfa 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx
@@ -6,7 +6,6 @@
import React, { Fragment, lazy } from 'react';
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
import { coreMock } from '../../../../../../../src/core/public/mocks';
-import { ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult, Alert, AlertAction } from '../../../types';
@@ -112,8 +111,6 @@ describe('action_form', () => {
};
describe('action_form in alert', () => {
- let wrapper: ReactWrapper;
-
async function setup(customActions?: AlertAction[]) {
const { loadAllActions } = jest.requireMock('../../lib/action_connector_api');
loadAllActions.mockResolvedValueOnce([
@@ -217,7 +214,7 @@ describe('action_form', () => {
mutedInstanceIds: [],
} as unknown) as Alert;
- wrapper = mountWithIntl(
+ const wrapper = mountWithIntl(
{
setActionIdByIndex={(id: string, index: number) => {
initialAlert.actions[index].id = id;
}}
+ actionGroups={[{ id: 'default', name: 'Default' }]}
+ setActionGroupIdByIndex={(group: string, index: number) => {
+ initialAlert.actions[index].group = group;
+ }}
setAlertProperty={(_updatedActions: AlertAction[]) => {}}
setActionParamsProperty={(key: string, value: any, index: number) =>
(initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value })
@@ -297,10 +298,12 @@ describe('action_form', () => {
await nextTick();
wrapper.update();
});
+
+ return wrapper;
}
it('renders available action cards', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find(
`[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
);
@@ -314,7 +317,7 @@ describe('action_form', () => {
});
it('does not render action types disabled by config', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find(
'[data-test-subj="disabled-by-config-ActionTypeSelectOption"]'
);
@@ -322,52 +325,72 @@ describe('action_form', () => {
});
it('render action types which is preconfigured only (disabled by config and with preconfigured connectors)', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
expect(actionOption.exists()).toBeTruthy();
});
+ it('renders available action groups for the selected action type', async () => {
+ const wrapper = await setup();
+ const actionOption = wrapper.find(
+ `[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
+ );
+ actionOption.first().simulate('click');
+ const actionGroupsSelect = wrapper.find(
+ `[data-test-subj="addNewActionConnectorActionGroup-0"]`
+ );
+ expect((actionGroupsSelect.first().props() as any).options).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "data-test-subj": "addNewActionConnectorActionGroup-0-option-default",
+ "inputDisplay": "Default",
+ "value": "default",
+ },
+ ]
+ `);
+ });
+
it('renders available connectors for the selected action type', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find(
`[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
);
actionOption.first().simulate('click');
const combobox = wrapper.find(`[data-test-subj="selectActionConnector-${actionType.id}"]`);
expect((combobox.first().props() as any).options).toMatchInlineSnapshot(`
- Array [
- Object {
- "id": "test",
- "key": "test",
- "label": "Test connector ",
- },
- Object {
- "id": "test2",
- "key": "test2",
- "label": "Test connector 2 (preconfigured)",
- },
- ]
- `);
+ Array [
+ Object {
+ "id": "test",
+ "key": "test",
+ "label": "Test connector ",
+ },
+ Object {
+ "id": "test2",
+ "key": "test2",
+ "label": "Test connector 2 (preconfigured)",
+ },
+ ]
+ `);
});
it('renders only preconfigured connectors for the selected preconfigured action type', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
actionOption.first().simulate('click');
const combobox = wrapper.find('[data-test-subj="selectActionConnector-preconfigured"]');
expect((combobox.first().props() as any).options).toMatchInlineSnapshot(`
- Array [
- Object {
- "id": "test3",
- "key": "test3",
- "label": "Preconfigured Only (preconfigured)",
- },
- ]
- `);
+ Array [
+ Object {
+ "id": "test3",
+ "key": "test3",
+ "label": "Preconfigured Only (preconfigured)",
+ },
+ ]
+ `);
});
it('does not render "Add connector" button for preconfigured only action type', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
actionOption.first().simulate('click');
const preconfigPannel = wrapper.find('[data-test-subj="alertActionAccordion-default"]');
@@ -378,7 +401,7 @@ describe('action_form', () => {
});
it('renders action types disabled by license', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find(
'[data-test-subj="disabled-by-license-ActionTypeSelectOption"]'
);
@@ -391,7 +414,7 @@ describe('action_form', () => {
});
it(`shouldn't render action types without params component`, async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find(
`[data-test-subj="${actionTypeWithoutParams.id}-ActionTypeSelectOption"]`
);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx
index 74432157f5659..3a7341afe3e07 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment, Suspense, useState, useEffect } from 'react';
+import React, { Fragment, useState, useEffect, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@@ -14,25 +14,13 @@ import {
EuiIcon,
EuiTitle,
EuiSpacer,
- EuiFormRow,
- EuiComboBox,
EuiKeyPadMenuItem,
- EuiAccordion,
- EuiButtonIcon,
- EuiEmptyPrompt,
- EuiButtonEmpty,
EuiToolTip,
- EuiIconTip,
EuiLink,
- EuiCallOut,
- EuiHorizontalRule,
- EuiText,
- EuiLoadingSpinner,
} from '@elastic/eui';
import { HttpSetup, ToastsSetup, ApplicationStart, DocLinksStart } from 'kibana/public';
import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api';
import {
- IErrorObject,
ActionTypeModel,
ActionTypeRegistryContract,
AlertAction,
@@ -43,15 +31,19 @@ import {
} from '../../../types';
import { SectionLoading } from '../../components/section_loading';
import { ConnectorAddModal } from './connector_add_modal';
+import { ActionTypeForm, ActionTypeFormProps } from './action_type_form';
+import { AddConnectorInline } from './connector_add_inline';
import { actionTypeCompare } from '../../lib/action_type_compare';
import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
import { VIEW_LICENSE_OPTIONS_LINK, DEFAULT_HIDDEN_ACTION_TYPES } from '../../../common/constants';
-import { hasSaveActionsCapability } from '../../lib/capabilities';
+import { ActionGroup } from '../../../../../alerts/common';
-interface ActionAccordionFormProps {
+export interface ActionAccordionFormProps {
actions: AlertAction[];
defaultActionGroupId: string;
+ actionGroups?: ActionGroup[];
setActionIdByIndex: (id: string, index: number) => void;
+ setActionGroupIdByIndex?: (group: string, index: number) => void;
setAlertProperty: (actions: AlertAction[]) => void;
setActionParamsProperty: (key: string, value: any, index: number) => void;
http: HttpSetup;
@@ -74,7 +66,9 @@ interface ActiveActionConnectorState {
export const ActionForm = ({
actions,
defaultActionGroupId,
+ actionGroups,
setActionIdByIndex,
+ setActionGroupIdByIndex,
setAlertProperty,
setActionParamsProperty,
http,
@@ -88,8 +82,6 @@ export const ActionForm = ({
capabilities,
docLinks,
}: ActionAccordionFormProps) => {
- const canSave = hasSaveActionsCapability(capabilities);
-
const [addModalVisible, setAddModalVisibility] = useState(false);
const [activeActionItem, setActiveActionItem] = useState(
undefined
@@ -101,6 +93,10 @@ export const ActionForm = ({
const [actionTypesIndex, setActionTypesIndex] = useState(undefined);
const [emptyActionsIds, setEmptyActionsIds] = useState([]);
+ const closeAddConnectorModal = useCallback(() => setAddModalVisibility(false), [
+ setAddModalVisibility,
+ ]);
+
// load action types
useEffect(() => {
(async () => {
@@ -183,359 +179,6 @@ export const ActionForm = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actions, connectors]);
- const preconfiguredMessage = i18n.translate(
- 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage',
- {
- defaultMessage: '(preconfigured)',
- }
- );
-
- const getSelectedOptions = (actionItemId: string) => {
- const selectedConnector = connectors.find((connector) => connector.id === actionItemId);
- if (
- !selectedConnector ||
- // if selected connector is not preconfigured and action type is for preconfiguration only,
- // do not show regular connectors of this type
- (actionTypesIndex &&
- !actionTypesIndex[selectedConnector.actionTypeId].enabledInConfig &&
- !selectedConnector.isPreconfigured)
- ) {
- return [];
- }
- const optionTitle = `${selectedConnector.name} ${
- selectedConnector.isPreconfigured ? preconfiguredMessage : ''
- }`;
- return [
- {
- label: optionTitle,
- value: optionTitle,
- id: actionItemId,
- 'data-test-subj': 'itemActionConnector',
- },
- ];
- };
-
- const getActionTypeForm = (
- actionItem: AlertAction,
- actionConnector: ActionConnector,
- actionParamsErrors: {
- errors: IErrorObject;
- },
- index: number
- ) => {
- if (!actionTypesIndex) {
- return null;
- }
-
- const actionType = actionTypesIndex[actionItem.actionTypeId];
-
- const optionsList = connectors
- .filter(
- (connectorItem) =>
- connectorItem.actionTypeId === actionItem.actionTypeId &&
- // include only enabled by config connectors or preconfigured
- (actionType.enabledInConfig || connectorItem.isPreconfigured)
- )
- .map(({ name, id, isPreconfigured }) => ({
- label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`,
- key: id,
- id,
- }));
- const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId);
- if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null;
- const ParamsFieldsComponent = actionTypeRegistered.actionParamsFields;
- const checkEnabledResult = checkActionFormActionTypeEnabled(
- actionTypesIndex[actionConnector.actionTypeId],
- connectors.filter((connector) => connector.isPreconfigured)
- );
-
- const accordionContent = checkEnabledResult.isEnabled ? (
-
-
-
-
- }
- labelAppend={
- canSave &&
- actionTypesIndex &&
- actionTypesIndex[actionConnector.actionTypeId].enabledInConfig ? (
- {
- setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
- setAddModalVisibility(true);
- }}
- >
-
-
- ) : null
- }
- >
- {
- setActionIdByIndex(selectedOptions[0].id ?? '', index);
- }}
- isClearable={false}
- />
-
-
-
-
- {ParamsFieldsComponent ? (
-
-
-
-
-
- }
- >
-
-
- ) : null}
-
- ) : (
- checkEnabledResult.messageCard
- );
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {checkEnabledResult.isEnabled === false && (
-
-
-
- )}
-
-
-
-
-
-
- }
- extraAction={
- {
- const updatedActions = actions.filter(
- (_item: AlertAction, i: number) => i !== index
- );
- setAlertProperty(updatedActions);
- setIsAddActionPanelOpen(
- updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length ===
- 0
- );
- setActiveActionItem(undefined);
- }}
- />
- }
- paddingSize="l"
- >
- {accordionContent}
-
-
-
- );
- };
-
- const getAddConnectorsForm = (actionItem: AlertAction, index: number) => {
- const actionTypeName = actionTypesIndex
- ? actionTypesIndex[actionItem.actionTypeId].name
- : actionItem.actionTypeId;
- const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId);
- if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null;
-
- const noConnectorsLabel = (
-
- );
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- extraAction={
- {
- const updatedActions = actions.filter(
- (_item: AlertAction, i: number) => i !== index
- );
- setAlertProperty(updatedActions);
- setIsAddActionPanelOpen(
- updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length ===
- 0
- );
- setActiveActionItem(undefined);
- }}
- />
- }
- paddingSize="l"
- >
- {canSave ? (
- actionItem.id === emptyId) ? (
- noConnectorsLabel
- ) : (
-
- )
- }
- actions={[
- {
- setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
- setAddModalVisibility(true);
- }}
- >
-
- ,
- ]}
- />
- ) : (
-
-
-
-
-
- )}
-
-
-
- );
- };
-
function addActionType(actionTypeModel: ActionTypeModel) {
if (!defaultActionGroupId) {
toastNotifications!.addDanger({
@@ -628,116 +271,172 @@ export const ActionForm = ({
});
}
- const alertActionsList = actions.map((actionItem: AlertAction, index: number) => {
- const actionConnector = connectors.find((field) => field.id === actionItem.id);
- // connectors doesn't exists
- if (!actionConnector) {
- return getAddConnectorsForm(actionItem, index);
- }
-
- const actionErrors: { errors: IErrorObject } = actionTypeRegistry
- .get(actionItem.actionTypeId)
- ?.validateParams(actionItem.params);
-
- return getActionTypeForm(actionItem, actionConnector, actionErrors, index);
- });
-
- return (
+ return isLoadingConnectors ? (
+
+
+
+ ) : (
- {isLoadingConnectors ? (
-
+
+
-
- ) : (
-
-
-
-
+
+
+ {actionTypesIndex &&
+ actions.map((actionItem: AlertAction, index: number) => {
+ const actionConnector = connectors.find((field) => field.id === actionItem.id);
+ // connectors doesn't exists
+ if (!actionConnector) {
+ return (
+ {
+ const updatedActions = actions.filter(
+ (_item: AlertAction, i: number) => i !== index
+ );
+ setAlertProperty(updatedActions);
+ setIsAddActionPanelOpen(
+ updatedActions.filter((item: AlertAction) => item.id !== actionItem.id)
+ .length === 0
+ );
+ setActiveActionItem(undefined);
+ }}
+ onAddConnector={() => {
+ setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
+ setAddModalVisibility(true);
+ }}
/>
-
-
-
- {alertActionsList}
- {isAddActionPanelOpen === false ? (
-
-
-
-
- setIsAddActionPanelOpen(true)}
- >
-
-
-
-
-
- ) : null}
- {isAddActionPanelOpen ? (
-
-
-
-
-
+ );
+ }
+
+ const actionParamsErrors: ActionTypeFormProps['actionParamsErrors'] = actionTypeRegistry
+ .get(actionItem.actionTypeId)
+ ?.validateParams(actionItem.params);
+
+ return (
+ {
+ setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
+ setAddModalVisibility(true);
+ }}
+ onConnectorSelected={(id: string) => {
+ setActionIdByIndex(id, index);
+ }}
+ onDeleteAction={() => {
+ const updatedActions = actions.filter(
+ (_item: AlertAction, i: number) => i !== index
+ );
+ setAlertProperty(updatedActions);
+ setIsAddActionPanelOpen(
+ updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length ===
+ 0
+ );
+ setActiveActionItem(undefined);
+ }}
+ />
+ );
+ })}
+
+ {isAddActionPanelOpen ? (
+
+
+
+
+
+
+
+
+
+ {hasDisabledByLicenseActionTypes && (
+
+
+
+
-
-
-
- {hasDisabledByLicenseActionTypes && (
-
-
-
-
-
-
-
-
-
- )}
-
-
-
- {isLoadingActionTypes ? (
-
-
-
- ) : (
- actionTypeNodes
- )}
-
-
- ) : null}
+
+
+
+
+ )}
+
+
+
+ {isLoadingActionTypes ? (
+
+
+
+ ) : (
+ actionTypeNodes
+ )}
+
+ ) : (
+
+
+ setIsAddActionPanelOpen(true)}
+ >
+
+
+
+
)}
- {actionTypesIndex && activeActionItem ? (
+ {actionTypesIndex && activeActionItem && addModalVisible ? (
{
connectors.push(savedAction);
setActionIdByIndex(savedAction.id, activeActionItem.index);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx
new file mode 100644
index 0000000000000..38468283b9c19
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx
@@ -0,0 +1,339 @@
+/*
+ * 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, { Fragment, Suspense, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiSpacer,
+ EuiFormRow,
+ EuiComboBox,
+ EuiAccordion,
+ EuiButtonIcon,
+ EuiButtonEmpty,
+ EuiIconTip,
+ EuiText,
+ EuiFormLabel,
+ EuiFormControlLayout,
+ EuiSuperSelect,
+ EuiLoadingSpinner,
+ EuiBadge,
+} from '@elastic/eui';
+import { IErrorObject, AlertAction, ActionTypeIndex, ActionConnector } from '../../../types';
+import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
+import { hasSaveActionsCapability } from '../../lib/capabilities';
+import { ActionAccordionFormProps } from './action_form';
+
+export type ActionTypeFormProps = {
+ actionItem: AlertAction;
+ actionConnector: ActionConnector;
+ actionParamsErrors: {
+ errors: IErrorObject;
+ };
+ index: number;
+ onAddConnector: () => void;
+ onConnectorSelected: (id: string) => void;
+ onDeleteAction: () => void;
+ setActionParamsProperty: (key: string, value: any, index: number) => void;
+ actionTypesIndex: ActionTypeIndex;
+ connectors: ActionConnector[];
+} & Pick<
+ ActionAccordionFormProps,
+ | 'defaultActionGroupId'
+ | 'actionGroups'
+ | 'setActionGroupIdByIndex'
+ | 'setActionParamsProperty'
+ | 'http'
+ | 'actionTypeRegistry'
+ | 'toastNotifications'
+ | 'docLinks'
+ | 'messageVariables'
+ | 'defaultActionMessage'
+ | 'capabilities'
+>;
+
+const preconfiguredMessage = i18n.translate(
+ 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage',
+ {
+ defaultMessage: '(preconfigured)',
+ }
+);
+
+export const ActionTypeForm = ({
+ actionItem,
+ actionConnector,
+ actionParamsErrors,
+ index,
+ onAddConnector,
+ onConnectorSelected,
+ onDeleteAction,
+ setActionParamsProperty,
+ actionTypesIndex,
+ connectors,
+ http,
+ toastNotifications,
+ docLinks,
+ capabilities,
+ actionTypeRegistry,
+ defaultActionGroupId,
+ defaultActionMessage,
+ messageVariables,
+ actionGroups,
+ setActionGroupIdByIndex,
+}: ActionTypeFormProps) => {
+ const [isOpen, setIsOpen] = useState(true);
+
+ const canSave = hasSaveActionsCapability(capabilities);
+ const getSelectedOptions = (actionItemId: string) => {
+ const selectedConnector = connectors.find((connector) => connector.id === actionItemId);
+ if (
+ !selectedConnector ||
+ // if selected connector is not preconfigured and action type is for preconfiguration only,
+ // do not show regular connectors of this type
+ (actionTypesIndex &&
+ !actionTypesIndex[selectedConnector.actionTypeId].enabledInConfig &&
+ !selectedConnector.isPreconfigured)
+ ) {
+ return [];
+ }
+ const optionTitle = `${selectedConnector.name} ${
+ selectedConnector.isPreconfigured ? preconfiguredMessage : ''
+ }`;
+ return [
+ {
+ label: optionTitle,
+ value: optionTitle,
+ id: actionItemId,
+ 'data-test-subj': 'itemActionConnector',
+ },
+ ];
+ };
+
+ const actionType = actionTypesIndex[actionItem.actionTypeId];
+
+ const optionsList = connectors
+ .filter(
+ (connectorItem) =>
+ connectorItem.actionTypeId === actionItem.actionTypeId &&
+ // include only enabled by config connectors or preconfigured
+ (actionType.enabledInConfig || connectorItem.isPreconfigured)
+ )
+ .map(({ name, id, isPreconfigured }) => ({
+ label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`,
+ key: id,
+ id,
+ }));
+ const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId);
+ if (!actionTypeRegistered) return null;
+
+ const ParamsFieldsComponent = actionTypeRegistered.actionParamsFields;
+ const checkEnabledResult = checkActionFormActionTypeEnabled(
+ actionTypesIndex[actionConnector.actionTypeId],
+ connectors.filter((connector) => connector.isPreconfigured)
+ );
+
+ const defaultActionGroup = actionGroups?.find(({ id }) => id === defaultActionGroupId);
+ const selectedActionGroup =
+ actionGroups?.find(({ id }) => id === actionItem.group) ?? defaultActionGroup;
+
+ const accordionContent = checkEnabledResult.isEnabled ? (
+
+ {actionGroups && selectedActionGroup && setActionGroupIdByIndex && (
+
+
+
+
+
+
+ }
+ >
+ ({
+ value,
+ inputDisplay: name,
+ 'data-test-subj': `addNewActionConnectorActionGroup-${index}-option-${value}`,
+ }))}
+ valueOfSelected={selectedActionGroup.id}
+ onChange={(group) => {
+ setActionGroupIdByIndex(group, index);
+ }}
+ />
+
+
+
+
+
+ )}
+
+
+
+ }
+ labelAppend={
+ canSave &&
+ actionTypesIndex &&
+ actionTypesIndex[actionConnector.actionTypeId].enabledInConfig ? (
+
+
+
+ ) : null
+ }
+ >
+ {
+ onConnectorSelected(selectedOptions[0].id ?? '');
+ }}
+ isClearable={false}
+ />
+
+
+
+
+ {ParamsFieldsComponent ? (
+
+
+
+
+
+ }
+ >
+
+
+ ) : null}
+
+ ) : (
+ checkEnabledResult.messageCard
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {selectedActionGroup && !isOpen && (
+
+ {selectedActionGroup.name}
+
+ )}
+
+ {checkEnabledResult.isEnabled === false && (
+
+
+
+ )}
+
+
+
+
+
+
+ }
+ extraAction={
+
+ }
+ >
+ {accordionContent}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx
new file mode 100644
index 0000000000000..97baf4a36cb4c
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx
@@ -0,0 +1,153 @@
+/*
+ * 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, { Fragment } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiSpacer,
+ EuiAccordion,
+ EuiButtonIcon,
+ EuiEmptyPrompt,
+ EuiCallOut,
+ EuiText,
+} from '@elastic/eui';
+import { AlertAction, ActionTypeIndex } from '../../../types';
+import { hasSaveActionsCapability } from '../../lib/capabilities';
+import { ActionAccordionFormProps } from './action_form';
+
+type AddConnectorInFormProps = {
+ actionTypesIndex: ActionTypeIndex;
+ actionItem: AlertAction;
+ index: number;
+ onAddConnector: () => void;
+ onDeleteConnector: () => void;
+ emptyActionsIds: string[];
+} & Pick;
+
+export const AddConnectorInline = ({
+ actionTypesIndex,
+ actionItem,
+ index,
+ onAddConnector,
+ onDeleteConnector,
+ actionTypeRegistry,
+ emptyActionsIds,
+ defaultActionGroupId,
+ capabilities,
+}: AddConnectorInFormProps) => {
+ const canSave = hasSaveActionsCapability(capabilities);
+
+ const actionTypeName = actionTypesIndex
+ ? actionTypesIndex[actionItem.actionTypeId].name
+ : actionItem.actionTypeId;
+ const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId);
+ if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null;
+
+ const noConnectorsLabel = (
+
+ );
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ extraAction={
+
+ }
+ paddingSize="l"
+ >
+ {canSave ? (
+ actionItem.id === emptyId) ? (
+ noConnectorsLabel
+ ) : (
+
+ )
+ }
+ actions={[
+
+
+ ,
+ ]}
+ />
+ ) : (
+
+
+
+
+
+ )}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx
index cba9eea3cf3f7..71a3936ed5055 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx
@@ -65,8 +65,7 @@ describe('connector_add_modal', () => {
const wrapper = mountWithIntl(
{}}
+ onClose={() => {}}
actionType={actionType}
http={deps!.http}
actionTypeRegistry={deps!.actionTypeRegistry}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx
index 13ec8395aa557..de27256bf566c 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx
@@ -32,8 +32,7 @@ import {
interface ConnectorAddModalProps {
actionType: ActionType;
- addModalVisible: boolean;
- setAddModalVisibility: React.Dispatch>;
+ onClose: () => void;
postSaveEventHandler?: (savedAction: ActionConnector) => void;
http: HttpSetup;
actionTypeRegistry: ActionTypeRegistryContract;
@@ -48,8 +47,7 @@ interface ConnectorAddModalProps {
export const ConnectorAddModal = ({
actionType,
- addModalVisible,
- setAddModalVisibility,
+ onClose,
postSaveEventHandler,
http,
toastNotifications,
@@ -79,14 +77,11 @@ export const ConnectorAddModal = ({
>(undefined);
const closeModal = useCallback(() => {
- setAddModalVisibility(false);
setConnector(initialConnector);
setServerError(undefined);
- }, [initialConnector, setAddModalVisibility]);
+ onClose();
+ }, [initialConnector, onClose]);
- if (!addModalVisible) {
- return null;
- }
const actionTypeModel = actionTypeRegistry.get(actionType.id);
const errors = {
...actionTypeModel?.validateConnector(connector).errors,
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx
index 9a637ea750f81..20ad9a8d7c701 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.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, { Fragment, useState, useEffect, Suspense } from 'react';
+import React, { Fragment, useState, useEffect, Suspense, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@@ -153,9 +153,17 @@ export const AlertForm = ({
setAlertTypeModel(alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null);
}, [alert, alertTypeRegistry]);
- const setAlertProperty = (key: string, value: any) => {
- dispatch({ command: { type: 'setProperty' }, payload: { key, value } });
- };
+ const setAlertProperty = useCallback(
+ (key: string, value: any) => {
+ dispatch({ command: { type: 'setProperty' }, payload: { key, value } });
+ },
+ [dispatch]
+ );
+
+ const setActions = useCallback(
+ (updatedActions: AlertAction[]) => setAlertProperty('actions', updatedActions),
+ [setAlertProperty]
+ );
const setAlertParams = (key: string, value: any) => {
dispatch({ command: { type: 'setAlertParams' }, payload: { key, value } });
@@ -169,9 +177,12 @@ export const AlertForm = ({
dispatch({ command: { type: 'setAlertActionProperty' }, payload: { key, value, index } });
};
- const setActionParamsProperty = (key: string, value: any, index: number) => {
- dispatch({ command: { type: 'setAlertActionParams' }, payload: { key, value, index } });
- };
+ const setActionParamsProperty = useCallback(
+ (key: string, value: any, index: number) => {
+ dispatch({ command: { type: 'setAlertActionParams' }, payload: { key, value, index } });
+ },
+ [dispatch]
+ );
const tagsOptions = alert.tags ? alert.tags.map((label: string) => ({ label })) : [];
@@ -202,6 +213,7 @@ export const AlertForm = ({
label={item.name}
onClick={() => {
setAlertProperty('alertTypeId', item.id);
+ setActions([]);
setAlertTypeModel(item);
setAlertProperty('params', {});
if (alertTypesIndex && alertTypesIndex.has(item.id)) {
@@ -289,26 +301,25 @@ export const AlertForm = ({
/>
) : null}
- {canShowActions && defaultActionGroupId ? (
+ {canShowActions &&
+ defaultActionGroupId &&
+ alertTypeModel &&
+ alertTypesIndex?.has(alert.alertTypeId) ? (
- a.name.toUpperCase().localeCompare(b.name.toUpperCase())
- )
- : undefined
- }
+ messageVariables={actionVariablesFromAlertType(
+ alertTypesIndex.get(alert.alertTypeId)!
+ ).sort((a, b) => a.name.toUpperCase().localeCompare(b.name.toUpperCase()))}
defaultActionGroupId={defaultActionGroupId}
+ actionGroups={alertTypesIndex.get(alert.alertTypeId)!.actionGroups}
setActionIdByIndex={(id: string, index: number) => setActionProperty('id', id, index)}
- setAlertProperty={(updatedActions: AlertAction[]) =>
- setAlertProperty('actions', updatedActions)
- }
- setActionParamsProperty={(key: string, value: any, index: number) =>
- setActionParamsProperty(key, value, index)
+ setActionGroupIdByIndex={(group: string, index: number) =>
+ setActionProperty('group', group, index)
}
+ setAlertProperty={setActions}
+ setActionParamsProperty={setActionParamsProperty}
http={http}
actionTypeRegistry={actionTypeRegistry}
defaultActionMessage={alertTypeModel?.defaultActionMessage}
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts
index 7d99d3635106d..ee0de582a9bff 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts
@@ -55,6 +55,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await nameInput.click();
}
+ async function defineAlwaysFiringAlert(alertName: string) {
+ await pageObjects.triggersActionsUI.clickCreateAlertButton();
+ await testSubjects.setValue('alertNameInput', alertName);
+ await testSubjects.click('test.always-firing-SelectOption');
+ }
+
describe('create alert', function () {
before(async () => {
await pageObjects.common.navigateToApp('triggersActions');
@@ -106,6 +112,57 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id));
});
+ it('should create an alert with actions in multiple groups', async () => {
+ const alertName = generateUniqueKey();
+ await defineAlwaysFiringAlert(alertName);
+
+ // create Slack connector and attach an action using it
+ await testSubjects.click('.slack-ActionTypeSelectOption');
+ await testSubjects.click('addNewActionConnectorButton-.slack');
+ const slackConnectorName = generateUniqueKey();
+ await testSubjects.setValue('nameInput', slackConnectorName);
+ await testSubjects.setValue('slackWebhookUrlInput', 'https://test');
+ await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)');
+ const createdConnectorToastTitle = await pageObjects.common.closeToast();
+ expect(createdConnectorToastTitle).to.eql(`Created '${slackConnectorName}'`);
+ await testSubjects.setValue('messageTextArea', 'test message ');
+ await (
+ await find.byCssSelector(
+ '[data-test-subj="alertActionAccordion-0"] [data-test-subj="messageTextArea"]'
+ )
+ ).type('some text ');
+
+ await testSubjects.click('addAlertActionButton');
+ await testSubjects.click('.slack-ActionTypeSelectOption');
+ await testSubjects.setValue('messageTextArea', 'test message ');
+ await (
+ await find.byCssSelector(
+ '[data-test-subj="alertActionAccordion-1"] [data-test-subj="messageTextArea"]'
+ )
+ ).type('some text ');
+
+ await testSubjects.click('addNewActionConnectorActionGroup-1');
+ await testSubjects.click('addNewActionConnectorActionGroup-1-option-other');
+
+ await testSubjects.click('saveAlertButton');
+ const toastTitle = await pageObjects.common.closeToast();
+ expect(toastTitle).to.eql(`Created alert "${alertName}"`);
+ await pageObjects.triggersActionsUI.searchAlerts(alertName);
+ const searchResultsAfterSave = await pageObjects.triggersActionsUI.getAlertsList();
+ expect(searchResultsAfterSave).to.eql([
+ {
+ name: alertName,
+ tagsText: '',
+ alertType: 'Always Firing',
+ interval: '1m',
+ },
+ ]);
+
+ // clean up created alert
+ const alertsToDelete = await getAlertsByName(alertName);
+ await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id));
+ });
+
it('should show save confirmation before creating alert with no actions', async () => {
const alertName = generateUniqueKey();
await defineAlert(alertName);
diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts
index e3927f6bfffb9..6f9d010378624 100644
--- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts
+++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts
@@ -78,6 +78,7 @@ function createAlwaysFiringAlertType(alerts: AlertingSetup) {
{ id: 'default', name: 'Default' },
{ id: 'other', name: 'Other' },
],
+ defaultActionGroupId: 'default',
producer: 'alerts',
async executor(alertExecutorOptions: any) {
const { services, state, params } = alertExecutorOptions;
From d1ef0d6704237cade5ff6a4246e42148dadb0b9e Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 9 Nov 2020 13:11:51 +0000
Subject: [PATCH 7/9] skip flaky suite (#57426)
---
.../functional_with_es_ssl/apps/triggers_actions_ui/details.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
index 9e4006681dc8d..1d86d95b7a796 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
@@ -306,7 +306,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
});
- describe('Alert Instances', function () {
+ // FLAKY: https://github.com/elastic/kibana/issues/57426
+ describe.skip('Alert Instances', function () {
const testRunUuid = uuid.v4();
let alert: any;
From f2f76e104af5d0515773c173475efda7d80c1d31 Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Mon, 9 Nov 2020 14:29:53 +0100
Subject: [PATCH 8/9] [ILM] Migrate Delete phase and name field to Form Lib
(#82834)
* remove use of legacy state system and legacy serialization
* remove legacy min_age input component and re-add missing import
* rename shared -> shared_fields for more clarity
* some more cleanup and fixing regressions on policy name for creating new policy from existing policy
* move extract policy static code to lib folder and remove "policies" dir from services
* fix jest tests and minor policy flyout inconsistency
* remove legacy helper
* fix client integration tests
* fix min for set index priority
* moved save policy function into edit policy section
* remove unused translations
* refactor form files to own edit_policy/form folder
* remove "fix errors" badge to fix UX - users can see errors in line before pressing save so the value of this badge has diminished
* fix i18n after removing phase error badge
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../edit_policy/edit_policy.helpers.tsx | 5 +
.../edit_policy/edit_policy.test.ts | 3 +-
.../__jest__/components/edit_policy.test.tsx | 242 ++++----
.../public/application/lib/policies.ts | 32 ++
.../sections/edit_policy/components/index.ts | 3 -
.../components/min_age_input_legacy.tsx | 263 ---------
.../components/phase_error_message.tsx | 19 -
.../phases/cold_phase/cold_phase.tsx | 23 +-
.../{ => delete_phase}/delete_phase.tsx | 94 +--
.../components/phases/delete_phase/index.ts | 7 +
.../components/phases/hot_phase/hot_phase.tsx | 12 +-
.../components/cloud_data_tier_callout.tsx | 0
.../components/data_tier_allocation.scss | 0
.../components/data_tier_allocation.tsx | 0
.../components/default_allocation_notice.tsx | 0
.../components/index.ts | 0
.../components/no_node_attributes_warning.tsx | 0
.../components/node_allocation.tsx | 9 +-
.../components/node_attrs_details.tsx | 0
.../components/node_data_provider.tsx | 0
.../components/types.ts | 0
.../data_tier_allocation_field.tsx | 0
.../data_tier_allocation_field/index.ts | 0
.../forcemerge_field.tsx | 6 +-
.../phases/{shared => shared_fields}/index.ts | 2 +
.../min_age_input_field/index.ts | 0
.../min_age_input_field.tsx | 0
.../min_age_input_field/util.ts | 0
.../set_priority_input.tsx | 9 +-
.../snapshot_policies_field.tsx} | 113 ++--
.../phases/warm_phase/warm_phase.tsx | 27 +-
.../components/policy_json_flyout.tsx | 22 +-
.../edit_policy/edit_policy.container.tsx | 23 +-
.../sections/edit_policy/edit_policy.tsx | 537 ++++++++----------
.../edit_policy/edit_policy_context.tsx | 12 +-
.../edit_policy/{ => form}/deserializer.ts | 21 +-
.../sections/edit_policy/form/index.ts | 13 +
.../{form_schema.ts => form/schema.ts} | 53 +-
.../edit_policy/{ => form}/serializer.ts | 24 +-
.../validations.ts} | 77 ++-
.../sections/edit_policy/i18n_texts.ts | 36 ++
.../edit_policy/save_policy.ts} | 15 +-
.../application/sections/edit_policy/types.ts | 5 +
.../services/policies/delete_phase.ts | 88 ---
.../policies/policy_serialization.test.ts | 198 -------
.../services/policies/policy_serialization.ts | 82 ---
.../services/policies/policy_validation.ts | 144 -----
.../public/shared_imports.ts | 3 +
.../translations/translations/ja-JP.json | 6 -
.../translations/translations/zh-CN.json | 6 -
50 files changed, 749 insertions(+), 1485 deletions(-)
create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/lib/policies.ts
delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input_legacy.tsx
delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{ => delete_phase}/delete_phase.tsx (50%)
create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/index.ts
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/cloud_data_tier_callout.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/data_tier_allocation.scss (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/data_tier_allocation.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/default_allocation_notice.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/index.ts (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/no_node_attributes_warning.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/node_allocation.tsx (90%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/node_attrs_details.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/node_data_provider.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/types.ts (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/data_tier_allocation_field.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/index.ts (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/forcemerge_field.tsx (94%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/index.ts (88%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/min_age_input_field/index.ts (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/min_age_input_field/min_age_input_field.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/min_age_input_field/util.ts (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/set_priority_input.tsx (83%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/{snapshot_policies.tsx => phases/shared_fields/snapshot_policies_field.tsx} (68%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{ => form}/deserializer.ts (82%)
create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{form_schema.ts => form/schema.ts} (90%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{ => form}/serializer.ts (90%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{form_validations.ts => form/validations.ts} (50%)
rename x-pack/plugins/index_lifecycle_management/public/application/{services/policies/policy_save.ts => sections/edit_policy/save_policy.ts} (84%)
delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts
delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts
delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts
delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
index 0b9f47e188d15..646978dd68153 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
@@ -221,6 +221,11 @@ export const setup = async () => {
setFreeze,
setIndexPriority: setIndexPriority('cold'),
},
+ delete: {
+ enable: enable('delete'),
+ setMinAgeValue: setMinAgeValue('delete'),
+ setMinAgeUnits: setMinAgeUnits('delete'),
+ },
},
};
};
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
index 11fadf51f27f8..4ee67d1ed8a19 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
@@ -367,7 +367,6 @@ describe('', () => {
expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([
{
label: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy,
- value: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy,
},
]);
});
@@ -412,7 +411,7 @@ describe('', () => {
test('wait for snapshot field should delete action if field is empty', async () => {
const { actions } = testBed;
- actions.setWaitForSnapshotPolicy('');
+ await actions.setWaitForSnapshotPolicy('');
await actions.savePolicy();
const expected = {
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx
index 4a3fedfb264ac..43910583ceec9 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx
@@ -20,27 +20,27 @@ import {
notificationServiceMock,
fatalErrorsServiceMock,
} from '../../../../../src/core/public/mocks';
+
import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks';
+
import { CloudSetup } from '../../../cloud/public';
import { EditPolicy } from '../../public/application/sections/edit_policy/edit_policy';
+import {
+ EditPolicyContextProvider,
+ EditPolicyContextValue,
+} from '../../public/application/sections/edit_policy/edit_policy_context';
+
import { KibanaContextProvider } from '../../public/shared_imports';
+
import { init as initHttp } from '../../public/application/services/http';
import { init as initUiMetric } from '../../public/application/services/ui_metric';
import { init as initNotification } from '../../public/application/services/notification';
import { PolicyFromES } from '../../common/types';
-import {
- positiveNumberRequiredMessage,
- policyNameRequiredMessage,
- policyNameStartsWithUnderscoreErrorMessage,
- policyNameContainsCommaErrorMessage,
- policyNameContainsSpaceErrorMessage,
- policyNameMustBeDifferentErrorMessage,
- policyNameAlreadyUsedErrorMessage,
-} from '../../public/application/services/policies/policy_validation';
import { i18nTexts } from '../../public/application/sections/edit_policy/i18n_texts';
import { editPolicyHelpers } from './helpers';
+import { defaultPolicy } from '../../public/application/constants';
// @ts-ignore
initHttp(axios.create({ adapter: axiosXhrAdapter }));
@@ -122,14 +122,11 @@ const noRollover = async (rendered: ReactWrapper) => {
const getNodeAttributeSelect = (rendered: ReactWrapper, phase: string) => {
return findTestSubject(rendered, `${phase}-selectedNodeAttrs`);
};
-const setPolicyName = (rendered: ReactWrapper, policyName: string) => {
+const setPolicyName = async (rendered: ReactWrapper, policyName: string) => {
const policyNameField = findTestSubject(rendered, 'policyNameField');
- policyNameField.simulate('change', { target: { value: policyName } });
- rendered.update();
-};
-const setPhaseAfterLegacy = (rendered: ReactWrapper, phase: string, after: string | number) => {
- const afterInput = rendered.find(`input#${phase}-selectedMinimumAge`);
- afterInput.simulate('change', { target: { value: after } });
+ await act(async () => {
+ policyNameField.simulate('change', { target: { value: policyName } });
+ });
rendered.update();
};
const setPhaseAfter = async (rendered: ReactWrapper, phase: string, after: string | number) => {
@@ -157,6 +154,32 @@ const save = async (rendered: ReactWrapper) => {
});
rendered.update();
};
+
+const MyComponent = ({
+ isCloudEnabled,
+ isNewPolicy,
+ policy: _policy,
+ existingPolicies,
+ getUrlForApp,
+ policyName,
+}: EditPolicyContextValue & { isCloudEnabled: boolean }) => {
+ return (
+
+
+
+
+
+ );
+};
+
describe('edit policy', () => {
beforeAll(() => {
jest.useFakeTimers();
@@ -179,14 +202,14 @@ describe('edit policy', () => {
beforeEach(() => {
component = (
-
-
-
+
);
({ http } = editPolicyHelpers.setup());
@@ -198,62 +221,78 @@ describe('edit policy', () => {
test('should show error when trying to save empty form', async () => {
const rendered = mountWithIntl(component);
await save(rendered);
- expectedErrorMessages(rendered, [policyNameRequiredMessage]);
+ expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameRequiredMessage]);
});
test('should show error when trying to save policy name with space', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'my policy');
- await save(rendered);
- expectedErrorMessages(rendered, [policyNameContainsSpaceErrorMessage]);
+ await setPolicyName(rendered, 'my policy');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]);
});
test('should show error when trying to save policy name that is already used', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'testy0');
- rendered.update();
- await save(rendered);
- expectedErrorMessages(rendered, [policyNameAlreadyUsedErrorMessage]);
+ await setPolicyName(rendered, 'testy0');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [
+ i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage,
+ ]);
});
test('should show error when trying to save as new policy but using the same name', async () => {
component = (
-
);
const rendered = mountWithIntl(component);
findTestSubject(rendered, 'saveAsNewSwitch').simulate('click');
rendered.update();
- setPolicyName(rendered, 'testy0');
- await save(rendered);
- expectedErrorMessages(rendered, [policyNameMustBeDifferentErrorMessage]);
+ await setPolicyName(rendered, 'testy0');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [
+ i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage,
+ ]);
});
test('should show error when trying to save policy name with comma', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'my,policy');
- await save(rendered);
- expectedErrorMessages(rendered, [policyNameContainsCommaErrorMessage]);
+ await setPolicyName(rendered, 'my,policy');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]);
});
test('should show error when trying to save policy name starting with underscore', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, '_mypolicy');
- await save(rendered);
- expectedErrorMessages(rendered, [policyNameStartsWithUnderscoreErrorMessage]);
+ await setPolicyName(rendered, '_mypolicy');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [
+ i18nTexts.editPolicy.errors.policyNameStartsWithUnderscoreErrorMessage,
+ ]);
});
test('should show correct json in policy flyout', async () => {
- const rendered = mountWithIntl(component);
+ const rendered = mountWithIntl(
+
+ );
await act(async () => {
findTestSubject(rendered, 'requestButton').simulate('click');
});
rendered.update();
+
const json = rendered.find(`code`).text();
- const expected = `PUT _ilm/policy/\n${JSON.stringify(
+ const expected = `PUT _ilm/policy/my-policy\n${JSON.stringify(
{
policy: {
phases: {
@@ -282,7 +321,7 @@ describe('edit policy', () => {
test('should show errors when trying to save with no max size and no max age', async () => {
const rendered = mountWithIntl(component);
expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeFalsy();
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored');
await act(async () => {
maxSizeInput.simulate('change', { target: { value: '' } });
@@ -298,7 +337,7 @@ describe('edit policy', () => {
});
test('should show number above 0 required error when trying to save with -1 for max size', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored');
await act(async () => {
maxSizeInput.simulate('change', { target: { value: '-1' } });
@@ -309,7 +348,7 @@ describe('edit policy', () => {
});
test('should show number above 0 required error when trying to save with 0 for max size', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored');
await act(async () => {
maxSizeInput.simulate('change', { target: { value: '-1' } });
@@ -319,7 +358,7 @@ describe('edit policy', () => {
});
test('should show number above 0 required error when trying to save with -1 for max age', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge');
await act(async () => {
maxAgeInput.simulate('change', { target: { value: '-1' } });
@@ -329,7 +368,7 @@ describe('edit policy', () => {
});
test('should show number above 0 required error when trying to save with 0 for max age', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge');
await act(async () => {
maxAgeInput.simulate('change', { target: { value: '0' } });
@@ -337,21 +376,21 @@ describe('edit policy', () => {
waitForFormLibValidation(rendered);
expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]);
});
- test('should show forcemerge input when rollover enabled', () => {
+ test('should show forcemerge input when rollover enabled', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeTruthy();
});
test('should hide forcemerge input when rollover is disabled', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await noRollover(rendered);
waitForFormLibValidation(rendered);
expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeFalsy();
});
test('should show positive number required above zero error when trying to save hot phase with 0 for force merge', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
act(() => {
findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click');
});
@@ -365,7 +404,7 @@ describe('edit policy', () => {
});
test('should show positive number above 0 required error when trying to save hot phase with -1 for force merge', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click');
rendered.update();
const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments');
@@ -379,7 +418,7 @@ describe('edit policy', () => {
test('should show positive number required error when trying to save with -1 for index priority', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await setPhaseIndexPriority(rendered, 'hot', '-1');
waitForFormLibValidation(rendered);
expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]);
@@ -397,7 +436,7 @@ describe('edit policy', () => {
test('should show number required error when trying to save empty warm phase', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '');
waitForFormLibValidation(rendered);
@@ -406,7 +445,7 @@ describe('edit policy', () => {
test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '0');
waitForFormLibValidation(rendered);
@@ -415,7 +454,7 @@ describe('edit policy', () => {
test('should show positive number required error when trying to save warm phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '-1');
waitForFormLibValidation(rendered);
@@ -424,7 +463,7 @@ describe('edit policy', () => {
test('should show positive number required error when trying to save warm phase with -1 for index priority', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '1');
await setPhaseAfter(rendered, 'warm', '-1');
@@ -434,7 +473,7 @@ describe('edit policy', () => {
test('should show positive number required above zero error when trying to save warm phase with 0 for shrink', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
act(() => {
findTestSubject(rendered, 'shrinkSwitch').simulate('click');
@@ -451,7 +490,7 @@ describe('edit policy', () => {
test('should show positive number above 0 required error when trying to save warm phase with -1 for shrink', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '1');
act(() => {
@@ -468,7 +507,7 @@ describe('edit policy', () => {
test('should show positive number required above zero error when trying to save warm phase with 0 for force merge', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '1');
act(() => {
@@ -485,7 +524,7 @@ describe('edit policy', () => {
test('should show positive number above 0 required error when trying to save warm phase with -1 for force merge', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '1');
await act(async () => {
@@ -503,7 +542,7 @@ describe('edit policy', () => {
server.respondImmediately = false;
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
@@ -517,7 +556,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'warm');
@@ -527,7 +566,7 @@ describe('edit policy', () => {
test('should show node attributes input when attributes exist', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'warm');
@@ -539,7 +578,7 @@ describe('edit policy', () => {
test('should show view node attributes link when attribute selected and show flyout when clicked', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'warm');
@@ -568,7 +607,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy();
@@ -581,7 +620,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeTruthy();
@@ -594,7 +633,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy();
@@ -611,7 +650,7 @@ describe('edit policy', () => {
test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
await setPhaseAfter(rendered, 'cold', '0');
waitForFormLibValidation(rendered);
@@ -621,7 +660,7 @@ describe('edit policy', () => {
test('should show positive number required error when trying to save cold phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
await setPhaseAfter(rendered, 'cold', '-1');
waitForFormLibValidation(rendered);
@@ -631,7 +670,7 @@ describe('edit policy', () => {
server.respondImmediately = false;
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
@@ -645,7 +684,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'cold');
@@ -655,7 +694,7 @@ describe('edit policy', () => {
test('should show node attributes input when attributes exist', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'cold');
@@ -667,7 +706,7 @@ describe('edit policy', () => {
test('should show view node attributes link when attribute selected and show flyout when clicked', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'cold');
@@ -689,7 +728,7 @@ describe('edit policy', () => {
test('should show positive number required error when trying to save with -1 for index priority', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
await setPhaseAfter(rendered, 'cold', '1');
await setPhaseIndexPriority(rendered, 'cold', '-1');
@@ -704,7 +743,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy();
@@ -717,7 +756,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeTruthy();
@@ -730,7 +769,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy();
@@ -740,20 +779,20 @@ describe('edit policy', () => {
test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'delete');
- setPhaseAfterLegacy(rendered, 'delete', '0');
- await save(rendered);
+ await setPhaseAfter(rendered, 'delete', '0');
+ waitForFormLibValidation(rendered);
expectedErrorMessages(rendered, []);
});
test('should show positive number required error when trying to save delete phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'delete');
- setPhaseAfterLegacy(rendered, 'delete', '-1');
- await save(rendered);
- expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
+ await setPhaseAfter(rendered, 'delete', '-1');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]);
});
});
describe('not on cloud', () => {
@@ -768,7 +807,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@@ -782,14 +821,13 @@ describe('edit policy', () => {
describe('on cloud', () => {
beforeEach(() => {
component = (
-
-
-
+
);
({ http } = editPolicyHelpers.setup());
({ server, httpRequestsMockHelpers } = http);
@@ -808,7 +846,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@@ -829,7 +867,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@@ -849,7 +887,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeTruthy();
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/policies.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/policies.ts
new file mode 100644
index 0000000000000..c4a91978a3765
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/policies.ts
@@ -0,0 +1,32 @@
+/*
+ * 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 { PolicyFromES } from '../../../common/types';
+
+export const splitSizeAndUnits = (field: string): { size: string; units: string } => {
+ let size = '';
+ let units = '';
+
+ const result = /(\d+)(\w+)/.exec(field);
+ if (result) {
+ size = result[1];
+ units = result[2];
+ }
+
+ return {
+ size,
+ units,
+ };
+};
+
+export const getPolicyByName = (
+ policies: PolicyFromES[] | null | undefined,
+ policyName: string = ''
+): PolicyFromES | undefined => {
+ if (policies && policies.length > 0) {
+ return policies.find((policy: PolicyFromES) => policy.name === policyName);
+ }
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts
index a04608338718e..326f6ff87dc3b 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts
@@ -7,11 +7,8 @@
export { ActiveBadge } from './active_badge';
export { ErrableFormRow } from './form_errors';
export { LearnMoreLink } from './learn_more_link';
-export { MinAgeInput } from './min_age_input_legacy';
export { OptionalLabel } from './optional_label';
-export { PhaseErrorMessage } from './phase_error_message';
export { PolicyJsonFlyout } from './policy_json_flyout';
-export { SnapshotPolicies } from './snapshot_policies';
export { DescribedFormField } from './described_form_field';
export * from './phases';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input_legacy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input_legacy.tsx
deleted file mode 100644
index 6fcf35b799289..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input_legacy.tsx
+++ /dev/null
@@ -1,263 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { i18n } from '@kbn/i18n';
-
-import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui';
-
-import { LearnMoreLink } from './learn_more_link';
-import { ErrableFormRow } from './form_errors';
-import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation';
-import { PhaseWithMinAge, Phases } from '../../../../../common/types';
-
-function getTimingLabelForPhase(phase: keyof Phases) {
- // NOTE: Hot phase isn't necessary, because indices begin in the hot phase.
- switch (phase) {
- case 'warm':
- return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeLabel', {
- defaultMessage: 'Timing for warm phase',
- });
-
- case 'cold':
- return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel', {
- defaultMessage: 'Timing for cold phase',
- });
-
- case 'delete':
- return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel', {
- defaultMessage: 'Timing for delete phase',
- });
- }
-}
-
-function getUnitsAriaLabelForPhase(phase: keyof Phases) {
- // NOTE: Hot phase isn't necessary, because indices begin in the hot phase.
- switch (phase) {
- case 'warm':
- return i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeUnitsAriaLabel',
- {
- defaultMessage: 'Units for timing of warm phase',
- }
- );
-
- case 'cold':
- return i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel',
- {
- defaultMessage: 'Units for timing of cold phase',
- }
- );
-
- case 'delete':
- return i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeUnitsAriaLabel',
- {
- defaultMessage: 'Units for timing of delete phase',
- }
- );
- }
-}
-
-interface Props {
- rolloverEnabled: boolean;
- errors?: PhaseValidationErrors;
- phase: keyof Phases & string;
- phaseData: T;
- setPhaseData: (dataKey: keyof T & string, value: string) => void;
- isShowingErrors: boolean;
-}
-
-export const MinAgeInput = ({
- rolloverEnabled,
- errors,
- phaseData,
- phase,
- setPhaseData,
- isShowingErrors,
-}: React.PropsWithChildren>): React.ReactElement => {
- let daysOptionLabel;
- let hoursOptionLabel;
- let minutesOptionLabel;
- let secondsOptionLabel;
- let millisecondsOptionLabel;
- let microsecondsOptionLabel;
- let nanosecondsOptionLabel;
-
- if (rolloverEnabled) {
- daysOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverDaysOptionLabel',
- {
- defaultMessage: 'days from rollover',
- }
- );
-
- hoursOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverHoursOptionLabel',
- {
- defaultMessage: 'hours from rollover',
- }
- );
- minutesOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverMinutesOptionLabel',
- {
- defaultMessage: 'minutes from rollover',
- }
- );
-
- secondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverSecondsOptionLabel',
- {
- defaultMessage: 'seconds from rollover',
- }
- );
- millisecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverMilliSecondsOptionLabel',
- {
- defaultMessage: 'milliseconds from rollover',
- }
- );
-
- microsecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverMicroSecondsOptionLabel',
- {
- defaultMessage: 'microseconds from rollover',
- }
- );
-
- nanosecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverNanoSecondsOptionLabel',
- {
- defaultMessage: 'nanoseconds from rollover',
- }
- );
- } else {
- daysOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationDaysOptionLabel',
- {
- defaultMessage: 'days from index creation',
- }
- );
-
- hoursOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationHoursOptionLabel',
- {
- defaultMessage: 'hours from index creation',
- }
- );
-
- minutesOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationMinutesOptionLabel',
- {
- defaultMessage: 'minutes from index creation',
- }
- );
-
- secondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationSecondsOptionLabel',
- {
- defaultMessage: 'seconds from index creation',
- }
- );
-
- millisecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationMilliSecondsOptionLabel',
- {
- defaultMessage: 'milliseconds from index creation',
- }
- );
-
- microsecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationMicroSecondsOptionLabel',
- {
- defaultMessage: 'microseconds from index creation',
- }
- );
-
- nanosecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationNanoSecondsOptionLabel',
- {
- defaultMessage: 'nanoseconds from index creation',
- }
- );
- }
-
- // check that these strings are valid properties
- const selectedMinimumAgeProperty = propertyof('selectedMinimumAge');
- const selectedMinimumAgeUnitsProperty = propertyof('selectedMinimumAgeUnits');
- return (
-
-
-
- }
- />
- }
- >
- {
- setPhaseData(selectedMinimumAgeProperty, e.target.value);
- }}
- min={0}
- />
-
-
-
-
- setPhaseData(selectedMinimumAgeUnitsProperty, e.target.value)}
- options={[
- {
- value: 'd',
- text: daysOptionLabel,
- },
- {
- value: 'h',
- text: hoursOptionLabel,
- },
- {
- value: 'm',
- text: minutesOptionLabel,
- },
- {
- value: 's',
- text: secondsOptionLabel,
- },
- {
- value: 'ms',
- text: millisecondsOptionLabel,
- },
- {
- value: 'micros',
- text: microsecondsOptionLabel,
- },
- {
- value: 'nanos',
- text: nanosecondsOptionLabel,
- },
- ]}
- />
-
-
-
- );
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx
deleted file mode 100644
index 750f68543f221..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React from 'react';
-import { EuiBadge } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-export const PhaseErrorMessage = ({ isShowingErrors }: { isShowingErrors: boolean }) => {
- return isShowingErrors ? (
-
-
-
- ) : null;
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx
index 84e955a91ad7c..b87243bd1a9a1 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx
@@ -13,19 +13,13 @@ import { EuiDescribedFormGroup, EuiTextColor } from '@elastic/eui';
import { Phases } from '../../../../../../../common/types';
-import {
- useFormData,
- useFormContext,
- UseField,
- ToggleField,
- NumericField,
-} from '../../../../../../shared_imports';
+import { useFormData, UseField, ToggleField, NumericField } from '../../../../../../shared_imports';
import { useEditPolicyContext } from '../../../edit_policy_context';
-import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, DescribedFormField } from '../../';
+import { LearnMoreLink, ActiveBadge, DescribedFormField } from '../../';
-import { MinAgeInputField, DataTierAllocationField, SetPriorityInput } from '../shared';
+import { MinAgeInputField, DataTierAllocationField, SetPriorityInput } from '../shared_fields';
const i18nTexts = {
dataTierAllocation: {
@@ -43,15 +37,13 @@ const formFieldPaths = {
};
export const ColdPhase: FunctionComponent = () => {
- const { originalPolicy } = useEditPolicyContext();
- const form = useFormContext();
+ const { policy } = useEditPolicyContext();
const [formData] = useFormData({
watch: [formFieldPaths.enabled],
});
const enabled = get(formData, formFieldPaths.enabled);
- const isShowingErrors = form.isValid === false;
return (
@@ -66,8 +58,7 @@ export const ColdPhase: FunctionComponent = () => {
defaultMessage="Cold phase"
/>
{' '}
- {enabled && !isShowingErrors ?
: null}
-
+ {enabled &&
}
}
titleSize="s"
@@ -128,9 +119,7 @@ export const ColdPhase: FunctionComponent = () => {
'xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel',
{ defaultMessage: 'Set replicas' }
),
- initialValue: Boolean(
- originalPolicy.phases.cold?.actions?.allocate?.number_of_replicas
- ),
+ initialValue: Boolean(policy.phases.cold?.actions?.allocate?.number_of_replicas),
}}
fullWidth
>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx
similarity index 50%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx
index 78ae66327654c..37323b97edc92 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx
@@ -7,53 +7,24 @@
import React, { FunctionComponent, Fragment } from 'react';
import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui';
+import { EuiDescribedFormGroup, EuiTextColor, EuiFormRow } from '@elastic/eui';
-import { DeletePhase as DeletePhaseInterface, Phases } from '../../../../../../common/types';
+import { useFormData, UseField, ToggleField } from '../../../../../../shared_imports';
-import { useFormData } from '../../../../../shared_imports';
+import { ActiveBadge, LearnMoreLink, OptionalLabel } from '../../index';
-import { PhaseValidationErrors } from '../../../../services/policies/policy_validation';
+import { MinAgeInputField, SnapshotPoliciesField } from '../shared_fields';
-import {
- ActiveBadge,
- LearnMoreLink,
- OptionalLabel,
- PhaseErrorMessage,
- MinAgeInput,
- SnapshotPolicies,
-} from '../';
-import { useRolloverPath } from './shared';
-
-const deleteProperty: keyof Phases = 'delete';
-const phaseProperty = (propertyName: keyof DeletePhaseInterface) => propertyName;
-
-interface Props {
- setPhaseData: (key: keyof DeletePhaseInterface & string, value: string | boolean) => void;
- phaseData: DeletePhaseInterface;
- isShowingErrors: boolean;
- errors?: PhaseValidationErrors;
- getUrlForApp: (
- appId: string,
- options?: {
- path?: string;
- absolute?: boolean;
- }
- ) => string;
-}
+const formFieldPaths = {
+ enabled: '_meta.delete.enabled',
+};
-export const DeletePhase: FunctionComponent = ({
- setPhaseData,
- phaseData,
- errors,
- isShowingErrors,
- getUrlForApp,
-}) => {
+export const DeletePhase: FunctionComponent = () => {
const [formData] = useFormData({
- watch: useRolloverPath,
+ watch: formFieldPaths.enabled,
});
- const hotPhaseRolloverEnabled = get(formData, useRolloverPath);
+ const enabled = get(formData, formFieldPaths.enabled);
return (
@@ -66,8 +37,7 @@ export const DeletePhase: FunctionComponent
= ({
defaultMessage="Delete phase"
/>
{' '}
- {phaseData.phaseEnabled && !isShowingErrors ? : null}
-
+ {enabled && }
}
titleSize="s"
@@ -79,39 +49,23 @@ export const DeletePhase: FunctionComponent = ({
defaultMessage="You no longer need your index. You can define when it is safe to delete it."
/>
-
- }
- id={`${deleteProperty}-${phaseProperty('phaseEnabled')}`}
- checked={phaseData.phaseEnabled}
- onChange={(e) => {
- setPhaseData(phaseProperty('phaseEnabled'), e.target.checked);
+
}
fullWidth
>
- {phaseData.phaseEnabled ? (
-
- errors={errors}
- phaseData={phaseData}
- phase={deleteProperty}
- isShowingErrors={isShowingErrors}
- setPhaseData={setPhaseData}
- rolloverEnabled={hotPhaseRolloverEnabled}
- />
- ) : (
-
- )}
+ {enabled && }
- {phaseData.phaseEnabled ? (
+ {enabled ? (
@@ -145,11 +99,7 @@ export const DeletePhase: FunctionComponent = ({
}
>
- setPhaseData(phaseProperty('waitForSnapshotPolicy'), value)}
- getUrlForApp={getUrlForApp}
- />
+
) : null}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/index.ts
new file mode 100644
index 0000000000000..488e4e26cfce0
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/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 { DeletePhase } from './delete_phase';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
index a184ddf5148b9..629c1388f61fb 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
@@ -19,7 +19,6 @@ import {
import { Phases } from '../../../../../../../common/types';
import {
- useFormContext,
useFormData,
UseField,
SelectField,
@@ -29,26 +28,24 @@ import {
import { i18nTexts } from '../../../i18n_texts';
-import { ROLLOVER_EMPTY_VALIDATION } from '../../../form_validations';
+import { ROLLOVER_EMPTY_VALIDATION } from '../../../form';
import { ROLLOVER_FORM_PATHS } from '../../../constants';
-import { LearnMoreLink, ActiveBadge, PhaseErrorMessage } from '../../';
+import { LearnMoreLink, ActiveBadge } from '../../';
-import { Forcemerge, SetPriorityInput, useRolloverPath } from '../shared';
+import { Forcemerge, SetPriorityInput, useRolloverPath } from '../shared_fields';
import { maxSizeStoredUnits, maxAgeUnits } from './constants';
const hotProperty: keyof Phases = 'hot';
export const HotPhase: FunctionComponent = () => {
- const form = useFormContext();
const [formData] = useFormData({
watch: useRolloverPath,
});
const isRolloverEnabled = get(formData, useRolloverPath);
- const isShowingErrors = form.isValid === false;
const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false);
return (
@@ -62,8 +59,7 @@ export const HotPhase: FunctionComponent = () => {
defaultMessage="Hot phase"
/>
{' '}
- {isShowingErrors ? null : }
-
+
}
titleSize="s"
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/cloud_data_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/cloud_data_tier_callout.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.scss
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.scss
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.scss
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/default_allocation_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/default_allocation_notice.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/index.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/no_node_attributes_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/no_node_attributes_warning.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx
similarity index 90%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_allocation.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx
index 407bb9ea92e85..c1676d7074dbc 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_allocation.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx
@@ -10,12 +10,8 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiText, EuiSpacer } from '@elastic/eui';
-import { PhaseWithAllocationAction } from '../../../../../../../../../common/types';
-
import { UseField, SelectField, useFormData } from '../../../../../../../../shared_imports';
-import { propertyof } from '../../../../../../../services/policies/policy_validation';
-
import { LearnMoreLink } from '../../../../learn_more_link';
import { NodeAttrsDetails } from './node_attrs_details';
@@ -61,9 +57,6 @@ export const NodeAllocation: FunctionComponent = ({ phase, nodes })
nodeOptions.sort((a, b) => a.value.localeCompare(b.value));
- // check that this string is a valid property
- const nodeAttrsProperty = propertyof('selectedNodeAttrs');
-
return (
<>
@@ -100,7 +93,7 @@ export const NodeAllocation: FunctionComponent = ({ phase, nodes })
) : undefined,
euiFieldProps: {
- 'data-test-subj': `${phase}-${nodeAttrsProperty}`,
+ 'data-test-subj': `${phase}-selectedNodeAttrs`,
options: [{ text: i18nTexts.doNotModifyAllocationOption, value: '' }].concat(
nodeOptions
),
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_attrs_details.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_attrs_details.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_attrs_details.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_attrs_details.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_data_provider.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_data_provider.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_data_provider.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_data_provider.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/types.ts
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/types.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/types.ts
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/data_tier_allocation_field.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/index.ts
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/index.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/index.ts
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx
similarity index 94%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx
index b410bd0e6b3b0..b05d49be497cd 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx
@@ -21,11 +21,11 @@ interface Props {
}
export const Forcemerge: React.FunctionComponent = ({ phase }) => {
- const { originalPolicy } = useEditPolicyContext();
+ const { policy } = useEditPolicyContext();
const initialToggleValue = useMemo(() => {
- return Boolean(originalPolicy.phases[phase]?.actions?.forcemerge);
- }, [originalPolicy, phase]);
+ return Boolean(policy.phases[phase]?.actions?.forcemerge);
+ }, [policy, phase]);
return (
= ({ phase }) => {
- const phaseIndexPriorityProperty = propertyof('phaseIndexPriority');
return (
= ({ phase }) => {
componentProps={{
fullWidth: false,
euiFieldProps: {
- 'data-test-subj': `${phase}-${phaseIndexPriorityProperty}`,
- min: 1,
+ 'data-test-subj': `${phase}-phaseIndexPriority`,
+ min: 0,
},
}}
/>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx
similarity index 68%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx
index cc2849b5c8e9c..e9f9f331e410a 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx
@@ -4,52 +4,39 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment } from 'react';
-
+import React from 'react';
+import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { ApplicationStart } from 'kibana/public';
import {
EuiButtonIcon,
EuiCallOut,
- EuiComboBox,
EuiComboBoxOptionOption,
EuiLink,
EuiSpacer,
} from '@elastic/eui';
-import { useLoadSnapshotPolicies } from '../../../services/api';
+import { UseField, ComboBoxField, useFormData } from '../../../../../../shared_imports';
+import { useLoadSnapshotPolicies } from '../../../../../services/api';
+import { useEditPolicyContext } from '../../../edit_policy_context';
+
+const waitForSnapshotFormField = 'phases.delete.actions.wait_for_snapshot.policy';
-interface Props {
- value: string;
- onChange: (value: string) => void;
- getUrlForApp: ApplicationStart['getUrlForApp'];
-}
-export const SnapshotPolicies: React.FunctionComponent = ({
- value,
- onChange,
- getUrlForApp,
-}) => {
+export const SnapshotPoliciesField: React.FunctionComponent = () => {
+ const { getUrlForApp } = useEditPolicyContext();
const { error, isLoading, data, resendRequest } = useLoadSnapshotPolicies();
+ const [formData] = useFormData({
+ watch: waitForSnapshotFormField,
+ });
+
+ const selectedSnapshotPolicy = get(formData, waitForSnapshotFormField);
const policies = data.map((name: string) => ({
label: name,
value: name,
}));
- const onComboChange = (options: EuiComboBoxOptionOption[]) => {
- if (options.length > 0) {
- onChange(options[0].label);
- } else {
- onChange('');
- }
- };
-
- const onCreateOption = (newValue: string) => {
- onChange(newValue);
- };
-
const getUrlForSnapshotPolicyWizard = () => {
return getUrlForApp('management', {
path: `data/snapshot_restore/add_policy`,
@@ -59,14 +46,14 @@ export const SnapshotPolicies: React.FunctionComponent = ({
let calloutContent;
if (error) {
calloutContent = (
-
+ <>
+ <>
= ({
}
)}
/>
-
+ >
}
>
= ({
defaultMessage="Refresh this field and enter the name of an existing snapshot policy."
/>
-
+ >
);
} else if (data.length === 0) {
calloutContent = (
-
+ <>
= ({
}}
/>
-
+ >
);
- } else if (value && !data.includes(value)) {
+ } else if (selectedSnapshotPolicy && !data.includes(selectedSnapshotPolicy)) {
calloutContent = (
-
+ <>
= ({
}}
/>
-
+ >
);
}
return (
-
-
+ path={waitForSnapshotFormField}>
+ {(field) => {
+ const singleSelectionArray: [selectedSnapshot?: string] = field.value
+ ? [field.value]
+ : [];
+
+ return (
+ {
+ field.setValue(newOption);
},
- ]
- : []
- }
- onChange={onComboChange}
- noSuggestions={!!(error || data.length === 0)}
- />
+ onChange: (options: EuiComboBoxOptionOption[]) => {
+ if (options.length > 0) {
+ field.setValue(options[0].label);
+ } else {
+ field.setValue('');
+ }
+ },
+ }}
+ />
+ );
+ }}
+
{calloutContent}
-
+ >
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx
index 06c16e8bdd5ab..94fd2ee9edaca 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx
@@ -17,23 +17,17 @@ import {
EuiDescribedFormGroup,
} from '@elastic/eui';
-import {
- useFormData,
- UseField,
- ToggleField,
- useFormContext,
- NumericField,
-} from '../../../../../../shared_imports';
+import { useFormData, UseField, ToggleField, NumericField } from '../../../../../../shared_imports';
import { Phases } from '../../../../../../../common/types';
-import { useRolloverPath, MinAgeInputField, Forcemerge, SetPriorityInput } from '../shared';
+import { useRolloverPath, MinAgeInputField, Forcemerge, SetPriorityInput } from '../shared_fields';
import { useEditPolicyContext } from '../../../edit_policy_context';
-import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, DescribedFormField } from '../../';
+import { LearnMoreLink, ActiveBadge, DescribedFormField } from '../../';
-import { DataTierAllocationField } from '../shared';
+import { DataTierAllocationField } from '../shared_fields';
const i18nTexts = {
shrinkLabel: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', {
@@ -54,8 +48,7 @@ const formFieldPaths = {
};
export const WarmPhase: FunctionComponent = () => {
- const { originalPolicy } = useEditPolicyContext();
- const form = useFormContext();
+ const { policy } = useEditPolicyContext();
const [formData] = useFormData({
watch: [useRolloverPath, formFieldPaths.enabled, formFieldPaths.warmPhaseOnRollover],
});
@@ -63,7 +56,6 @@ export const WarmPhase: FunctionComponent = () => {
const enabled = get(formData, formFieldPaths.enabled);
const hotPhaseRolloverEnabled = get(formData, useRolloverPath);
const warmPhaseOnRollover = get(formData, formFieldPaths.warmPhaseOnRollover);
- const isShowingErrors = form.isValid === false;
return (
@@ -77,8 +69,7 @@ export const WarmPhase: FunctionComponent = () => {
defaultMessage="Warm phase"
/>
{' '}
- {enabled && !isShowingErrors ?
: null}
-
+ {enabled &&
}
}
titleSize="s"
@@ -161,9 +152,7 @@ export const WarmPhase: FunctionComponent = () => {
'xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel',
{ defaultMessage: 'Set replicas' }
),
- initialValue: Boolean(
- originalPolicy.phases.warm?.actions?.allocate?.number_of_replicas
- ),
+ initialValue: Boolean(policy.phases.warm?.actions?.allocate?.number_of_replicas),
}}
fullWidth
>
@@ -203,7 +192,7 @@ export const WarmPhase: FunctionComponent = () => {
'data-test-subj': 'shrinkSwitch',
label: i18nTexts.shrinkLabel,
'aria-label': i18nTexts.shrinkLabel,
- initialValue: Boolean(originalPolicy.phases.warm?.actions?.shrink),
+ initialValue: Boolean(policy.phases.warm?.actions?.shrink),
}}
fullWidth
>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx
index 7098b018d6dfd..a8b1680ebde07 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx
@@ -7,7 +7,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-
import {
EuiButtonEmpty,
EuiCodeBlock,
@@ -25,19 +24,15 @@ import {
import { SerializedPolicy } from '../../../../../common/types';
import { useFormContext, useFormData } from '../../../../shared_imports';
+
import { FormInternal } from '../types';
interface Props {
- legacyPolicy: SerializedPolicy;
close: () => void;
policyName: string;
}
-export const PolicyJsonFlyout: React.FunctionComponent = ({
- policyName,
- close,
- legacyPolicy,
-}) => {
+export const PolicyJsonFlyout: React.FunctionComponent = ({ policyName, close }) => {
/**
* policy === undefined: we are checking validity
* policy === null: we have determined the policy is invalid
@@ -51,20 +46,11 @@ export const PolicyJsonFlyout: React.FunctionComponent = ({
const updatePolicy = useCallback(async () => {
setPolicy(undefined);
if (await validateForm()) {
- const p = getFormData() as SerializedPolicy;
- setPolicy({
- ...legacyPolicy,
- phases: {
- ...legacyPolicy.phases,
- hot: p.phases.hot,
- warm: p.phases.warm,
- cold: p.phases.cold,
- },
- });
+ setPolicy(getFormData() as SerializedPolicy);
} else {
setPolicy(null);
}
- }, [setPolicy, getFormData, legacyPolicy, validateForm]);
+ }, [setPolicy, getFormData, validateForm]);
useEffect(() => {
updatePolicy();
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx
index c82a420b74857..ebef80871b83d 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx
@@ -12,8 +12,11 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { useKibana } from '../../../shared_imports';
import { useLoadPoliciesList } from '../../services/api';
+import { getPolicyByName } from '../../lib/policies';
+import { defaultPolicy } from '../../constants';
import { EditPolicy as PresentationComponent } from './edit_policy';
+import { EditPolicyContextProvider } from './edit_policy_context';
interface RouterProps {
policyName: string;
@@ -44,6 +47,7 @@ export const EditPolicy: React.FunctionComponent {
breadcrumbService.setBreadcrumbs('editPolicy');
}, [breadcrumbService]);
+
if (isLoading) {
return (
+
+
+
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx
index 5397f5da2d6bb..1abbe884c2dc2 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment, useEffect, useState, useCallback, useMemo } from 'react';
+import React, { Fragment, useEffect, useState, useMemo } from 'react';
+import { get } from 'lodash';
import { RouteComponentProps } from 'react-router-dom';
@@ -16,7 +17,6 @@ import {
EuiButton,
EuiButtonEmpty,
EuiDescribedFormGroup,
- EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
@@ -30,31 +30,13 @@ import {
EuiTitle,
} from '@elastic/eui';
-import { useForm, Form } from '../../../shared_imports';
+import { useForm, Form, UseField, TextField, useFormData } from '../../../shared_imports';
import { toasts } from '../../services/notification';
-import { LegacyPolicy, PolicyFromES, SerializedPolicy } from '../../../../common/types';
-
-import { defaultPolicy } from '../../constants';
-
-import {
- validatePolicy,
- ValidationErrors,
- findFirstError,
-} from '../../services/policies/policy_validation';
-
-import { savePolicy } from '../../services/policies/policy_save';
+import { savePolicy } from './save_policy';
import {
- deserializePolicy,
- getPolicyByName,
- initializeNewPolicy,
- legacySerializePolicy,
-} from '../../services/policies/policy_serialization';
-
-import {
- ErrableFormRow,
LearnMoreLink,
PolicyJsonFlyout,
ColdPhase,
@@ -63,93 +45,66 @@ import {
WarmPhase,
} from './components';
-import { schema } from './form_schema';
-import { deserializer } from './deserializer';
-import { createSerializer } from './serializer';
+import { schema, deserializer, createSerializer, createPolicyNameValidations } from './form';
-import { EditPolicyContextProvider } from './edit_policy_context';
+import { useEditPolicyContext } from './edit_policy_context';
+import { FormInternal } from './types';
export interface Props {
- policies: PolicyFromES[];
- policyName: string;
- getUrlForApp: (
- appId: string,
- options?: {
- path?: string;
- absolute?: boolean;
- }
- ) => string;
history: RouteComponentProps['history'];
}
-const mergeAllSerializedPolicies = (
- serializedPolicy: SerializedPolicy,
- legacySerializedPolicy: SerializedPolicy
-): SerializedPolicy => {
- return {
- ...legacySerializedPolicy,
- phases: {
- ...legacySerializedPolicy.phases,
- hot: serializedPolicy.phases.hot,
- warm: serializedPolicy.phases.warm,
- cold: serializedPolicy.phases.cold,
- },
- };
-};
+const policyNamePath = 'name';
-export const EditPolicy: React.FunctionComponent = ({
- policies,
- policyName,
- history,
- getUrlForApp,
-}) => {
+export const EditPolicy: React.FunctionComponent = ({ history }) => {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
- const [isShowingErrors, setIsShowingErrors] = useState(false);
- const [errors, setErrors] = useState();
const [isShowingPolicyJsonFlyout, setIsShowingPolicyJsonFlyout] = useState(false);
-
- const existingPolicy = getPolicyByName(policies, policyName);
+ const {
+ isNewPolicy,
+ policy: currentPolicy,
+ existingPolicies,
+ policyName,
+ } = useEditPolicyContext();
const serializer = useMemo(() => {
- return createSerializer(existingPolicy?.policy);
- }, [existingPolicy?.policy]);
+ return createSerializer(isNewPolicy ? undefined : currentPolicy);
+ }, [isNewPolicy, currentPolicy]);
- const originalPolicy = existingPolicy?.policy ?? defaultPolicy;
+ const [saveAsNew, setSaveAsNew] = useState(isNewPolicy);
+ const originalPolicyName: string = isNewPolicy ? '' : policyName!;
const { form } = useForm({
schema,
- defaultValue: originalPolicy,
+ defaultValue: {
+ ...currentPolicy,
+ name: originalPolicyName,
+ },
deserializer,
serializer,
});
- const [policy, setPolicy] = useState(() =>
- existingPolicy ? deserializePolicy(existingPolicy) : initializeNewPolicy(policyName)
+ const [formData] = useFormData({ form, watch: policyNamePath });
+ const currentPolicyName = get(formData, policyNamePath);
+
+ const policyNameValidations = useMemo(
+ () =>
+ createPolicyNameValidations({
+ originalPolicyName,
+ policies: existingPolicies,
+ saveAsNewPolicy: saveAsNew,
+ }),
+ [originalPolicyName, existingPolicies, saveAsNew]
);
- const isNewPolicy: boolean = !Boolean(existingPolicy);
- const [saveAsNew, setSaveAsNew] = useState(isNewPolicy);
- const originalPolicyName: string = existingPolicy ? existingPolicy.name : '';
-
const backToPolicyList = () => {
history.push('/policies');
};
const submit = async () => {
- setIsShowingErrors(true);
- const { data: formLibPolicy, isValid: newIsValid } = await form.submit();
- const [legacyIsValid, validationErrors] = validatePolicy(
- saveAsNew,
- policy,
- policies,
- originalPolicyName
- );
- setErrors(validationErrors);
-
- const isValid = legacyIsValid && newIsValid;
+ const { data: policy, isValid } = await form.submit();
if (!isValid) {
toasts.addDanger(
@@ -157,22 +112,11 @@ export const EditPolicy: React.FunctionComponent = ({
defaultMessage: 'Please fix the errors on this page.',
})
);
- // This functionality will not be required for once form lib is fully adopted for this form
- // because errors are reported as fields are edited.
- if (!legacyIsValid) {
- const firstError = findFirstError(validationErrors);
- const errorRowId = `${firstError ? firstError.replace('.', '-') : ''}-row`;
- const element = document.getElementById(errorRowId);
- if (element) {
- element.scrollIntoView({ block: 'center', inline: 'nearest' });
- }
- }
} else {
- const readSerializedPolicy = () => {
- const legacySerializedPolicy = legacySerializePolicy(policy, existingPolicy?.policy);
- return mergeAllSerializedPolicies(formLibPolicy, legacySerializedPolicy);
- };
- const success = await savePolicy(readSerializedPolicy, isNewPolicy || saveAsNew);
+ const success = await savePolicy(
+ { ...policy, name: saveAsNew ? currentPolicyName : originalPolicyName },
+ isNewPolicy || saveAsNew
+ );
if (success) {
backToPolicyList();
}
@@ -183,248 +127,217 @@ export const EditPolicy: React.FunctionComponent = ({
setIsShowingPolicyJsonFlyout(!isShowingPolicyJsonFlyout);
};
- const setPhaseData = useCallback(
- (phase: keyof LegacyPolicy['phases'], key: string, value: any) => {
- setPolicy((nextPolicy) => ({
- ...nextPolicy,
- phases: {
- ...nextPolicy.phases,
- [phase]: { ...nextPolicy.phases[phase], [key]: value },
- },
- }));
- },
- [setPolicy]
- );
-
- const setDeletePhaseData = useCallback(
- (key: string, value: any) => setPhaseData('delete', key, value),
- [setPhaseData]
- );
-
return (
-
-
-
-
-
-
- {isNewPolicy
- ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', {
- defaultMessage: 'Create an index lifecycle policy',
- })
- : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', {
- defaultMessage: 'Edit index lifecycle policy {originalPolicyName}',
- values: { originalPolicyName },
- })}
-
-
-
-
+ }
+ titleSize="s"
+ fullWidth
+ >
+
+ path={policyNamePath}
+ config={{
+ label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameLabel', {
+ defaultMessage: 'Policy name',
+ }),
+ helpText: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage',
+ {
+ defaultMessage:
+ 'A policy name cannot start with an underscore and cannot contain a question mark or a space.',
+ }
+ ),
+ validations: policyNameValidations,
+ }}
+ component={TextField}
+ componentProps={{
+ fullWidth: false,
+ euiFieldProps: {
+ 'data-test-subj': 'policyNameField',
+ },
+ }}
+ />
+
+ ) : null}
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- 0
- }
- getUrlForApp={getUrlForApp}
- setPhaseData={setDeletePhaseData}
- phaseData={policy.phases.delete}
- />
+
-
-
-
-
-
-
-
- {saveAsNew ? (
-
- ) : (
-
- )}
-
-
-
-
-
+
+
+
+
+
+
+
+ {saveAsNew ? (
-
-
-
-
-
-
-
- {isShowingPolicyJsonFlyout ? (
-
- ) : (
+ ) : (
+
+ )}
+
+
+
+
+
- )}
-
-
-
-
- {isShowingPolicyJsonFlyout ? (
- setIsShowingPolicyJsonFlyout(false)}
- />
- ) : null}
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {isShowingPolicyJsonFlyout ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {isShowingPolicyJsonFlyout ? (
+ setIsShowingPolicyJsonFlyout(false)}
+ />
+ ) : null}
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx
index 4748c26d6cec1..da5f940b1b6c8 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx
@@ -5,10 +5,16 @@
*/
import React, { createContext, ReactChild, useContext } from 'react';
-import { SerializedPolicy } from '../../../../common/types';
+import { ApplicationStart } from 'kibana/public';
-interface EditPolicyContextValue {
- originalPolicy: SerializedPolicy;
+import { PolicyFromES, SerializedPolicy } from '../../../../common/types';
+
+export interface EditPolicyContextValue {
+ isNewPolicy: boolean;
+ policy: SerializedPolicy;
+ existingPolicies: PolicyFromES[];
+ getUrlForApp: ApplicationStart['getUrlForApp'];
+ policyName?: string;
}
const EditPolicyContext = createContext(null as any);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts
similarity index 82%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts
index f0294a5391d21..5af8807f2dec8 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts
@@ -6,17 +6,17 @@
import { produce } from 'immer';
-import { SerializedPolicy } from '../../../../common/types';
+import { SerializedPolicy } from '../../../../../common/types';
-import { splitSizeAndUnits } from '../../services/policies/policy_serialization';
+import { splitSizeAndUnits } from '../../../lib/policies';
-import { determineDataTierAllocationType } from '../../lib';
+import { determineDataTierAllocationType } from '../../../lib';
-import { FormInternal } from './types';
+import { FormInternal } from '../types';
export const deserializer = (policy: SerializedPolicy): FormInternal => {
const {
- phases: { hot, warm, cold },
+ phases: { hot, warm, cold, delete: deletePhase },
} = policy;
const _meta: FormInternal['_meta'] = {
@@ -37,6 +37,9 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => {
dataTierAllocationType: determineDataTierAllocationType(cold?.actions),
freezeEnabled: Boolean(cold?.actions?.freeze),
},
+ delete: {
+ enabled: Boolean(deletePhase),
+ },
};
return produce(
@@ -86,6 +89,14 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => {
draft._meta.cold.minAgeUnit = minAge.units;
}
}
+
+ if (draft.phases.delete) {
+ if (draft.phases.delete.min_age) {
+ const minAge = splitSizeAndUnits(draft.phases.delete.min_age);
+ draft.phases.delete.min_age = minAge.size;
+ draft._meta.delete.minAgeUnit = minAge.units;
+ }
+ }
}
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts
new file mode 100644
index 0000000000000..82fa478832582
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { deserializer } from './deserializer';
+
+export { createSerializer } from './serializer';
+
+export { schema } from './schema';
+
+export * from './validations';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts
similarity index 90%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts
index 070f03f74b954..4d20db4018740 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts
@@ -6,18 +6,19 @@
import { i18n } from '@kbn/i18n';
-import { FormSchema, fieldValidators } from '../../../shared_imports';
-import { defaultSetPriority, defaultPhaseIndexPriority } from '../../constants';
+import { FormSchema, fieldValidators } from '../../../../shared_imports';
+import { defaultSetPriority, defaultPhaseIndexPriority } from '../../../constants';
-import { FormInternal } from './types';
+import { FormInternal } from '../types';
import {
ifExistsNumberGreaterThanZero,
ifExistsNumberNonNegative,
rolloverThresholdsValidator,
-} from './form_validations';
+ minAgeValidator,
+} from './validations';
-import { i18nTexts } from './i18n_texts';
+import { i18nTexts } from '../i18n_texts';
const { emptyField, numberGreaterThanField } = fieldValidators;
@@ -97,6 +98,18 @@ export const schema: FormSchema = {
label: i18nTexts.editPolicy.allocationNodeAttributeFieldLabel,
},
},
+ delete: {
+ enabled: {
+ defaultValue: false,
+ label: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.deletePhase.activateWarmPhaseSwitchLabel',
+ { defaultMessage: 'Activate delete phase' }
+ ),
+ },
+ minAgeUnit: {
+ defaultValue: 'd',
+ },
+ },
},
phases: {
hot: {
@@ -177,15 +190,7 @@ export const schema: FormSchema = {
defaultValue: '0',
validations: [
{
- validator: (arg) =>
- numberGreaterThanField({
- than: 0,
- allowEquality: true,
- message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired,
- })({
- ...arg,
- value: arg.value === '' ? -Infinity : parseInt(arg.value, 10),
- }),
+ validator: minAgeValidator,
},
],
},
@@ -256,15 +261,7 @@ export const schema: FormSchema = {
defaultValue: '0',
validations: [
{
- validator: (arg) =>
- numberGreaterThanField({
- than: 0,
- allowEquality: true,
- message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired,
- })({
- ...arg,
- value: arg.value === '' ? -Infinity : parseInt(arg.value, 10),
- }),
+ validator: minAgeValidator,
},
],
},
@@ -292,5 +289,15 @@ export const schema: FormSchema = {
},
},
},
+ delete: {
+ min_age: {
+ defaultValue: '0',
+ validations: [
+ {
+ validator: minAgeValidator,
+ },
+ ],
+ },
+ },
},
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts
similarity index 90%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts
index 564b5a2c4e397..2274efda426ad 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts
@@ -4,12 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { isEmpty } from 'lodash';
+import { isEmpty, isNumber } from 'lodash';
-import { SerializedPolicy, SerializedActionWithAllocation } from '../../../../common/types';
+import { SerializedPolicy, SerializedActionWithAllocation } from '../../../../../common/types';
-import { FormInternal, DataAllocationMetaFields } from './types';
-import { isNumber } from '../../services/policies/policy_serialization';
+import { FormInternal, DataAllocationMetaFields } from '../types';
const serializeAllocateAction = (
{ dataTierAllocationType, allocationNodeAttribute }: DataAllocationMetaFields,
@@ -165,5 +164,22 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => (
}
}
+ /**
+ * DELETE PHASE SERIALIZATION
+ */
+ if (policy.phases.delete) {
+ if (policy.phases.delete.min_age) {
+ policy.phases.delete.min_age = `${policy.phases.delete.min_age}${_meta.delete.minAgeUnit}`;
+ }
+
+ if (originalPolicy?.phases.delete?.actions) {
+ const { wait_for_snapshot: __, ...rest } = originalPolicy.phases.delete.actions;
+ policy.phases.delete.actions = {
+ ...policy.phases.delete.actions,
+ ...rest,
+ };
+ }
+ }
+
return policy;
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts
similarity index 50%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts
index 9c855ccb41624..f2e26a552efc9 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts
@@ -4,13 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { fieldValidators, ValidationFunc } from '../../../shared_imports';
+import { fieldValidators, ValidationFunc, ValidationConfig } from '../../../../shared_imports';
-import { ROLLOVER_FORM_PATHS } from './constants';
+import { ROLLOVER_FORM_PATHS } from '../constants';
-import { i18nTexts } from './i18n_texts';
+import { i18nTexts } from '../i18n_texts';
+import { PolicyFromES } from '../../../../../common/types';
+import { FormInternal } from '../types';
-const { numberGreaterThanField } = fieldValidators;
+const { numberGreaterThanField, containsCharsField, emptyField, startsWithField } = fieldValidators;
const createIfNumberExistsValidator = ({
than,
@@ -46,7 +48,7 @@ export const ifExistsNumberNonNegative = createIfNumberExistsValidator({
* A special validation type used to keep track of validation errors for
* the rollover threshold values not being set (e.g., age and doc count)
*/
-export const ROLLOVER_EMPTY_VALIDATION = 'EMPTY';
+export const ROLLOVER_EMPTY_VALIDATION = 'ROLLOVER_EMPTY_VALIDATION';
/**
* An ILM policy requires that for rollover a value must be set for one of the threshold values.
@@ -87,3 +89,68 @@ export const rolloverThresholdsValidator: ValidationFunc = ({ form }) => {
fields[ROLLOVER_FORM_PATHS.maxSize].clearErrors(ROLLOVER_EMPTY_VALIDATION);
}
};
+
+export const minAgeValidator: ValidationFunc = (arg) =>
+ numberGreaterThanField({
+ than: 0,
+ allowEquality: true,
+ message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired,
+ })({
+ ...arg,
+ value: arg.value === '' ? -Infinity : parseInt(arg.value, 10),
+ });
+
+export const createPolicyNameValidations = ({
+ policies,
+ saveAsNewPolicy,
+ originalPolicyName,
+}: {
+ policies: PolicyFromES[];
+ saveAsNewPolicy: boolean;
+ originalPolicyName?: string;
+}): Array> => {
+ return [
+ {
+ validator: emptyField(i18nTexts.editPolicy.errors.policyNameRequiredMessage),
+ },
+ {
+ validator: startsWithField({
+ message: i18nTexts.editPolicy.errors.policyNameStartsWithUnderscoreErrorMessage,
+ char: '_',
+ }),
+ },
+ {
+ validator: containsCharsField({
+ message: i18nTexts.editPolicy.errors.policyNameContainsInvalidChars,
+ chars: [',', ' '],
+ }),
+ },
+ {
+ validator: (arg) => {
+ const policyName = arg.value;
+ if (window.TextEncoder && new window.TextEncoder().encode(policyName).length > 255) {
+ return {
+ message: i18nTexts.editPolicy.errors.policyNameTooLongErrorMessage,
+ };
+ }
+ },
+ },
+ {
+ validator: (arg) => {
+ const policyName = arg.value;
+ if (saveAsNewPolicy && policyName === originalPolicyName) {
+ return {
+ message: i18nTexts.editPolicy.errors.policyNameMustBeDifferentErrorMessage,
+ };
+ } else if (policyName !== originalPolicyName) {
+ const policyNames = policies.map((existingPolicy) => existingPolicy.name);
+ if (policyNames.includes(policyName)) {
+ return {
+ message: i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage,
+ };
+ }
+ }
+ },
+ },
+ ];
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
index 1fba69b7634ae..ccd5d3a568fe3 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
@@ -98,6 +98,42 @@ export const i18nTexts = {
defaultMessage: 'Only non-negative numbers are allowed.',
}
),
+ policyNameContainsInvalidChars: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.errors.policyNameContainsInvalidCharsError',
+ {
+ defaultMessage: 'A policy name cannot contain spaces or commas.',
+ }
+ ),
+ policyNameAlreadyUsedErrorMessage: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError',
+ {
+ defaultMessage: 'That policy name is already used.',
+ }
+ ),
+ policyNameMustBeDifferentErrorMessage: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.differentPolicyNameRequiredError',
+ {
+ defaultMessage: 'The policy name must be different.',
+ }
+ ),
+ policyNameRequiredMessage: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError',
+ {
+ defaultMessage: 'A policy name is required.',
+ }
+ ),
+ policyNameStartsWithUnderscoreErrorMessage: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError',
+ {
+ defaultMessage: 'A policy name cannot start with an underscore.',
+ }
+ ),
+ policyNameTooLongErrorMessage: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError',
+ {
+ defaultMessage: 'A policy name cannot be longer than 255 bytes.',
+ }
+ ),
},
},
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/save_policy.ts
similarity index 84%
rename from x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/save_policy.ts
index 9cf622e830cb2..e2ab6a8817ef6 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/save_policy.ts
@@ -3,23 +3,22 @@
* 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';
import { METRIC_TYPE } from '@kbn/analytics';
import { SerializedPolicy } from '../../../../common/types';
-import { savePolicy as savePolicyApi } from '../api';
-import { showApiError } from '../api_errors';
-import { getUiMetricsForPhases, trackUiMetric } from '../ui_metric';
+
import { UIM_POLICY_CREATE, UIM_POLICY_UPDATE } from '../../constants';
-import { toasts } from '../notification';
+
+import { toasts } from '../../services/notification';
+import { savePolicy as savePolicyApi } from '../../services/api';
+import { getUiMetricsForPhases, trackUiMetric } from '../../services/ui_metric';
+import { showApiError } from '../../services/api_errors';
export const savePolicy = async (
- readSerializedPolicy: () => SerializedPolicy,
+ serializedPolicy: SerializedPolicy,
isNew: boolean
): Promise => {
- const serializedPolicy = readSerializedPolicy();
-
try {
await savePolicyApi(serializedPolicy);
} catch (err) {
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts
index 1884f8dbc0619..dc3d8a640e682 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts
@@ -38,6 +38,10 @@ interface ColdPhaseMetaFields extends DataAllocationMetaFields, MinAgeField {
freezeEnabled: boolean;
}
+interface DeletePhaseMetaFields extends MinAgeField {
+ enabled: boolean;
+}
+
/**
* Describes the shape of data after deserialization.
*/
@@ -50,5 +54,6 @@ export interface FormInternal extends SerializedPolicy {
hot: HotPhaseMetaFields;
warm: WarmPhaseMetaFields;
cold: ColdPhaseMetaFields;
+ delete: DeletePhaseMetaFields;
};
}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts
deleted file mode 100644
index 6ada039d45cd9..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { DeletePhase, SerializedDeletePhase } from '../../../../common/types';
-import { serializedPhaseInitialization } from '../../constants';
-import { isNumber, splitSizeAndUnits } from './policy_serialization';
-import {
- numberRequiredMessage,
- PhaseValidationErrors,
- positiveNumberRequiredMessage,
-} from './policy_validation';
-
-const deletePhaseInitialization: DeletePhase = {
- phaseEnabled: false,
- selectedMinimumAge: '0',
- selectedMinimumAgeUnits: 'd',
- waitForSnapshotPolicy: '',
-};
-
-export const deletePhaseFromES = (phaseSerialized?: SerializedDeletePhase): DeletePhase => {
- const phase = { ...deletePhaseInitialization };
- if (phaseSerialized === undefined || phaseSerialized === null) {
- return phase;
- }
-
- phase.phaseEnabled = true;
- if (phaseSerialized.min_age) {
- const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age);
- phase.selectedMinimumAge = minAge;
- phase.selectedMinimumAgeUnits = minAgeUnits;
- }
-
- if (phaseSerialized.actions) {
- const actions = phaseSerialized.actions;
-
- if (actions.wait_for_snapshot) {
- phase.waitForSnapshotPolicy = actions.wait_for_snapshot.policy;
- }
- }
-
- return phase;
-};
-
-export const deletePhaseToES = (
- phase: DeletePhase,
- originalEsPhase?: SerializedDeletePhase
-): SerializedDeletePhase => {
- if (!originalEsPhase) {
- originalEsPhase = { ...serializedPhaseInitialization };
- }
- const esPhase = { ...originalEsPhase };
-
- if (isNumber(phase.selectedMinimumAge)) {
- esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`;
- }
-
- esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {};
-
- if (phase.waitForSnapshotPolicy) {
- esPhase.actions.wait_for_snapshot = {
- policy: phase.waitForSnapshotPolicy,
- };
- } else {
- delete esPhase.actions.wait_for_snapshot;
- }
-
- return esPhase;
-};
-
-export const validateDeletePhase = (phase: DeletePhase): PhaseValidationErrors => {
- if (!phase.phaseEnabled) {
- return {};
- }
-
- const phaseErrors = {} as PhaseValidationErrors;
-
- // min age needs to be a positive number
- if (!isNumber(phase.selectedMinimumAge)) {
- phaseErrors.selectedMinimumAge = [numberRequiredMessage];
- } else if (parseInt(phase.selectedMinimumAge, 10) < 0) {
- phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage];
- }
-
- return { ...phaseErrors };
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts
deleted file mode 100644
index 19481b39a2c80..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-// Prefer importing entire lodash library, e.g. import { get } from "lodash"
-// eslint-disable-next-line no-restricted-imports
-import cloneDeep from 'lodash/cloneDeep';
-import { deserializePolicy, legacySerializePolicy } from './policy_serialization';
-import { defaultNewDeletePhase } from '../../constants';
-
-describe('Policy serialization', () => {
- test('serialize a policy using "default" data allocation', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {
- hot: { actions: {} },
- },
- }
- )
- ).toEqual({
- name: 'test',
- phases: {},
- });
- });
-
- test('serialize a policy using "custom" data allocation', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {
- hot: { actions: {} },
- },
- }
- )
- ).toEqual({
- name: 'test',
- phases: {},
- });
- });
-
- test('serialize a policy using "custom" data allocation with no node attributes', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {
- hot: { actions: {} },
- },
- }
- )
- ).toEqual({
- // There should be no allocation action in any phases...
- name: 'test',
- phases: {},
- });
- });
-
- test('serialize a policy using "none" data allocation with no node attributes', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {
- hot: { actions: {} },
- },
- }
- )
- ).toEqual({
- // There should be no allocation action in any phases...
- name: 'test',
- phases: {},
- });
- });
-
- test('serialization does not alter the original policy', () => {
- const originalPolicy = {
- name: 'test',
- phases: {},
- };
-
- const originalClone = cloneDeep(originalPolicy);
-
- const deserializedPolicy = {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- };
-
- legacySerializePolicy(deserializedPolicy, originalPolicy);
- expect(originalPolicy).toEqual(originalClone);
- });
-
- test('serialize a policy using "best_compression" codec for forcemerge', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {
- hot: { actions: {} },
- },
- }
- )
- ).toEqual({
- name: 'test',
- phases: {},
- });
- });
-
- test('de-serialize a policy using "best_compression" codec for forcemerge', () => {
- expect(
- deserializePolicy({
- modified_date: Date.now().toString(),
- name: 'test',
- version: 1,
- policy: {
- name: 'test',
- phases: {
- hot: {
- actions: {
- rollover: {
- max_age: '30d',
- max_size: '50gb',
- },
- forcemerge: {
- max_num_segments: 1,
- index_codec: 'best_compression',
- },
- set_priority: {
- priority: 100,
- },
- },
- },
- },
- },
- })
- ).toEqual({
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- });
- });
-
- test('delete "best_compression" codec for forcemerge if disabled in UI', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {},
- }
- )
- ).toEqual({
- name: 'test',
- phases: {},
- });
- });
-});
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts
deleted file mode 100644
index 55e9d88dcd383..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { LegacyPolicy, PolicyFromES, SerializedPolicy } from '../../../../common/types';
-
-import { defaultNewDeletePhase, serializedPhaseInitialization } from '../../constants';
-
-import { deletePhaseFromES, deletePhaseToES } from './delete_phase';
-
-export const splitSizeAndUnits = (field: string): { size: string; units: string } => {
- let size = '';
- let units = '';
-
- const result = /(\d+)(\w+)/.exec(field);
- if (result) {
- size = result[1];
- units = result[2];
- }
-
- return {
- size,
- units,
- };
-};
-
-export const isNumber = (value: any): boolean => value !== '' && value !== null && isFinite(value);
-
-export const getPolicyByName = (
- policies: PolicyFromES[] | null | undefined,
- policyName: string = ''
-): PolicyFromES | undefined => {
- if (policies && policies.length > 0) {
- return policies.find((policy: PolicyFromES) => policy.name === policyName);
- }
-};
-
-export const initializeNewPolicy = (newPolicyName: string = ''): LegacyPolicy => {
- return {
- name: newPolicyName,
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- };
-};
-
-export const deserializePolicy = (policy: PolicyFromES): LegacyPolicy => {
- const {
- name,
- policy: { phases },
- } = policy;
-
- return {
- name,
- phases: {
- delete: deletePhaseFromES(phases.delete),
- },
- };
-};
-
-export const legacySerializePolicy = (
- policy: LegacyPolicy,
- originalEsPolicy: SerializedPolicy = {
- name: policy.name,
- phases: { hot: { ...serializedPhaseInitialization } },
- }
-): SerializedPolicy => {
- const serializedPolicy = {
- name: policy.name,
- phases: {},
- } as SerializedPolicy;
-
- if (policy.phases.delete.phaseEnabled) {
- serializedPolicy.phases.delete = deletePhaseToES(
- policy.phases.delete,
- originalEsPolicy.phases.delete
- );
- }
- return serializedPolicy;
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts
deleted file mode 100644
index 79c909c433f33..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { i18n } from '@kbn/i18n';
-import { DeletePhase, LegacyPolicy, PolicyFromES } from '../../../../common/types';
-import { validateDeletePhase } from './delete_phase';
-
-export const propertyof = (propertyName: keyof T & string) => propertyName;
-
-export const numberRequiredMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.numberRequiredError',
- {
- defaultMessage: 'A number is required.',
- }
-);
-
-// TODO validation includes 0 -> should be non-negative number?
-export const positiveNumberRequiredMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberRequiredError',
- {
- defaultMessage: 'Only positive numbers are allowed.',
- }
-);
-
-export const positiveNumbersAboveZeroErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberAboveZeroRequiredError',
- {
- defaultMessage: 'Only numbers above 0 are allowed.',
- }
-);
-
-export const policyNameRequiredMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError',
- {
- defaultMessage: 'A policy name is required.',
- }
-);
-
-export const policyNameStartsWithUnderscoreErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError',
- {
- defaultMessage: 'A policy name cannot start with an underscore.',
- }
-);
-export const policyNameContainsCommaErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError',
- {
- defaultMessage: 'A policy name cannot include a comma.',
- }
-);
-export const policyNameContainsSpaceErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError',
- {
- defaultMessage: 'A policy name cannot include a space.',
- }
-);
-
-export const policyNameTooLongErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError',
- {
- defaultMessage: 'A policy name cannot be longer than 255 bytes.',
- }
-);
-export const policyNameMustBeDifferentErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.differentPolicyNameRequiredError',
- {
- defaultMessage: 'The policy name must be different.',
- }
-);
-export const policyNameAlreadyUsedErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError',
- {
- defaultMessage: 'That policy name is already used.',
- }
-);
-export type PhaseValidationErrors = {
- [P in keyof Partial]: string[];
-};
-
-export interface ValidationErrors {
- delete: PhaseValidationErrors;
- policyName: string[];
-}
-
-export const validatePolicy = (
- saveAsNew: boolean,
- policy: LegacyPolicy,
- policies: PolicyFromES[],
- originalPolicyName: string
-): [boolean, ValidationErrors] => {
- const policyNameErrors: string[] = [];
- if (!policy.name) {
- policyNameErrors.push(policyNameRequiredMessage);
- } else {
- if (policy.name.startsWith('_')) {
- policyNameErrors.push(policyNameStartsWithUnderscoreErrorMessage);
- }
- if (policy.name.includes(',')) {
- policyNameErrors.push(policyNameContainsCommaErrorMessage);
- }
- if (policy.name.includes(' ')) {
- policyNameErrors.push(policyNameContainsSpaceErrorMessage);
- }
- if (window.TextEncoder && new window.TextEncoder().encode(policy.name).length > 255) {
- policyNameErrors.push(policyNameTooLongErrorMessage);
- }
-
- if (saveAsNew && policy.name === originalPolicyName) {
- policyNameErrors.push(policyNameMustBeDifferentErrorMessage);
- } else if (policy.name !== originalPolicyName) {
- const policyNames = policies.map((existingPolicy) => existingPolicy.name);
- if (policyNames.includes(policy.name)) {
- policyNameErrors.push(policyNameAlreadyUsedErrorMessage);
- }
- }
- }
-
- const deletePhaseErrors = validateDeletePhase(policy.phases.delete);
- const isValid = policyNameErrors.length === 0 && Object.keys(deletePhaseErrors).length === 0;
- return [
- isValid,
- {
- policyName: [...policyNameErrors],
- delete: deletePhaseErrors,
- },
- ];
-};
-
-export const findFirstError = (errors?: ValidationErrors): string | undefined => {
- if (!errors) {
- return;
- }
-
- if (errors.policyName.length > 0) {
- return propertyof('policyName');
- }
-
- if (Object.keys(errors.delete).length > 0) {
- return `${propertyof('delete')}.${Object.keys(errors.delete)[0]}`;
- }
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts
index 023aeba57aa7a..a127574d5bad0 100644
--- a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts
@@ -18,6 +18,7 @@ export {
getFieldValidityAndErrorMessage,
useFormContext,
FormSchema,
+ ValidationConfig,
} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/forms/helpers';
@@ -27,6 +28,8 @@ export {
NumericField,
SelectField,
SuperSelectField,
+ ComboBoxField,
+ TextField,
} from '../../../../src/plugins/es_ui_shared/static/forms/components';
export { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public';
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index e7784846598e4..baa4f37791007 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -9119,24 +9119,18 @@
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesReloadButton": "再試行",
"xpack.indexLifecycleMgmt.editPolicy.nodeDetailsLoadingFailedTitle": "ノード属性詳細を読み込めません",
"xpack.indexLifecycleMgmt.editPolicy.nodeDetailsReloadButton": "再試行",
- "xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字が必要です。",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "コールドフェーズのタイミング",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel": "コールドフェーズのタイミングの単位",
"xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel": "削除フェーズのタイミング",
"xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeUnitsAriaLabel": "削除フェーズのタイミングの単位",
- "xpack.indexLifecycleMgmt.editPolicy.phaseErrorMessage": "エラーを修正してください",
"xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeLabel": "ウォームフェーズのタイミング",
"xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeUnitsAriaLabel": "ウォームフェーズのタイミングの単位",
"xpack.indexLifecycleMgmt.editPolicy.policiesLoading": "ポリシーを読み込み中…",
"xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError": "このポリシー名は既に使用されています。",
- "xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError": "ポリシー名にはコンマを使用できません。",
- "xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError": "ポリシー名にはスペースを使用できません。",
"xpack.indexLifecycleMgmt.editPolicy.policyNameLabel": "ポリシー名",
"xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError": "ポリシー名が必要です。",
"xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError": "ポリシー名の頭にアンダーラインを使用することはできません。",
"xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError": "ポリシー名は 255 バイト未満である必要があります。",
- "xpack.indexLifecycleMgmt.editPolicy.positiveNumberAboveZeroRequiredError": "0 よりも大きい数字のみ使用できます。",
- "xpack.indexLifecycleMgmt.editPolicy.positiveNumberRequiredError": "プラスの数字のみ使用できます。",
"xpack.indexLifecycleMgmt.editPolicy.rolloverDaysOptionLabel": "ロールオーバーからの経過日数",
"xpack.indexLifecycleMgmt.editPolicy.rolloverHoursOptionLabel": "ロールオーバーからの経過時間数",
"xpack.indexLifecycleMgmt.editPolicy.rolloverMicroSecondsOptionLabel": "ロールオーバーからの経過時間(マイクロ秒)",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index f3cd662bacba7..c4274524928fd 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -9128,24 +9128,18 @@
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesReloadButton": "重试",
"xpack.indexLifecycleMgmt.editPolicy.nodeDetailsLoadingFailedTitle": "无法加载节点属性详情",
"xpack.indexLifecycleMgmt.editPolicy.nodeDetailsReloadButton": "重试",
- "xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字必填。",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "冷阶段计时",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel": "冷阶段计时单位",
"xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel": "删除阶段计时",
"xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeUnitsAriaLabel": "删除阶段计时单位",
- "xpack.indexLifecycleMgmt.editPolicy.phaseErrorMessage": "修复错误",
"xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeLabel": "温阶段计时",
"xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeUnitsAriaLabel": "温阶段计时单位",
"xpack.indexLifecycleMgmt.editPolicy.policiesLoading": "正在加载策略……",
"xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError": "该策略名称已被使用。",
- "xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError": "策略名称不能包含逗号。",
- "xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError": "策略名称不能包含空格。",
"xpack.indexLifecycleMgmt.editPolicy.policyNameLabel": "策略名称",
"xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError": "策略名称必填。",
"xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError": "策略名称不能以下划线开头。",
"xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError": "策略名称的长度不能大于 255 字节。",
- "xpack.indexLifecycleMgmt.editPolicy.positiveNumberAboveZeroRequiredError": "仅允许使用 0 以上的数字。",
- "xpack.indexLifecycleMgmt.editPolicy.positiveNumberRequiredError": "仅允许使用正数。",
"xpack.indexLifecycleMgmt.editPolicy.rolloverDaysOptionLabel": "天(自滚动更新)",
"xpack.indexLifecycleMgmt.editPolicy.rolloverHoursOptionLabel": "小时(自滚动更新)",
"xpack.indexLifecycleMgmt.editPolicy.rolloverMicroSecondsOptionLabel": "微秒(自滚动更新)",
From c78cf35ba8bfdff5f61d3932cbacd45a69e4f773 Mon Sep 17 00:00:00 2001
From: Dhruv Bodani
Date: Mon, 9 Nov 2020 19:05:05 +0530
Subject: [PATCH 9/9] Added `defaultActionMessage` to index threshold alert UI
type definition (#80936)
* resolves https://github.com/elastic/kibana/issues/78148
Adds a `defaultActionMessage` to the index threshold alert, so that the `message` parameter for actions will be pre-filled with a useful message
---
.../index_threshold/action_context.test.ts | 3 ++
.../index_threshold/action_context.ts | 7 ++--
.../index_threshold/alert_type.test.ts | 4 ++
.../alert_types/index_threshold/alert_type.ts | 12 ++++++
.../builtin_alert_types/threshold/index.ts | 7 +++-
.../index_threshold/alert.ts | 41 ++++++++++++++-----
.../alert_create_flyout.ts | 5 ++-
7 files changed, 63 insertions(+), 16 deletions(-)
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts
index 3f5addb77cb33..48847686828a9 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts
@@ -25,6 +25,7 @@ describe('ActionContext', () => {
date: '2020-01-01T00:00:00.000Z',
group: '[group]',
value: 42,
+ function: 'count > 4',
};
const context = addMessages({ name: '[alert-name]' }, base, params);
expect(context.title).toMatchInlineSnapshot(
@@ -53,6 +54,7 @@ describe('ActionContext', () => {
date: '2020-01-01T00:00:00.000Z',
group: '[group]',
value: 42,
+ function: 'avg([aggField]) > 4.2',
};
const context = addMessages({ name: '[alert-name]' }, base, params);
expect(context.title).toMatchInlineSnapshot(
@@ -80,6 +82,7 @@ describe('ActionContext', () => {
date: '2020-01-01T00:00:00.000Z',
group: '[group]',
value: 4,
+ function: 'count between 4,5',
};
const context = addMessages({ name: '[alert-name]' }, base, params);
expect(context.title).toMatchInlineSnapshot(
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts
index 5135e31e9322c..9bb0df9d07fd4 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts
@@ -27,6 +27,8 @@ export interface BaseActionContext extends AlertInstanceContext {
date: string;
// the value that met the threshold
value: number;
+ // the function that is used
+ function: string;
}
export function addMessages(
@@ -42,9 +44,6 @@ export function addMessages(
},
});
- const agg = params.aggField ? `${params.aggType}(${params.aggField})` : `${params.aggType}`;
- const humanFn = `${agg} ${params.thresholdComparator} ${params.threshold.join(',')}`;
-
const window = `${params.timeWindowSize}${params.timeWindowUnit}`;
const message = i18n.translate(
'xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription',
@@ -55,7 +54,7 @@ export function addMessages(
name: alertInfo.name,
group: baseContext.group,
value: baseContext.value,
- function: humanFn,
+ function: baseContext.function,
window,
date: baseContext.date,
},
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts
index 2f0cf3cbbcd16..d75f3af22ab06 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts
@@ -46,6 +46,10 @@ describe('alertType', () => {
"description": "The value that exceeded the threshold.",
"name": "value",
},
+ Object {
+ "description": "A string describing the threshold comparator and threshold",
+ "name": "function",
+ },
],
"params": Array [
Object {
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts
index 2a1ed429b7fe1..e0a9cd981dac0 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts
@@ -83,6 +83,13 @@ export function getAlertType(service: Service): AlertType {
return {
@@ -107,6 +114,7 @@ export function getAlertType(service: Service): AlertType import('./expression')),
validate: validateExpression,
+ defaultActionMessage: i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinAlertTypes.threshold.alertDefaultActionMessage',
+ {
+ defaultMessage: `alert \\{\\{alertName\\}\\} group \\{\\{context.group\\}\\} value \\{\\{context.value\\}\\} exceeded threshold \\{\\{context.function\\}\\} over \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\} on \\{\\{context.date\\}\\}`,
+ }
+ ),
requiresAppContext: false,
};
}
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts
index 92db0458c0639..c05fa6cf051ff 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts
@@ -15,6 +15,7 @@ import {
ObjectRemover,
} from '../../../../../common/lib';
import { createEsDocuments } from './create_test_data';
+import { getAlertType } from '../../../../../../../plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/';
const ALERT_TYPE_ID = '.index-threshold';
const ACTION_TYPE_ID = '.index';
@@ -26,6 +27,8 @@ const ALERT_INTERVALS_TO_WRITE = 5;
const ALERT_INTERVAL_SECONDS = 3;
const ALERT_INTERVAL_MILLIS = ALERT_INTERVAL_SECONDS * 1000;
+const DefaultActionMessage = getAlertType().defaultActionMessage;
+
// eslint-disable-next-line import/no-default-export
export default function alertTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
@@ -62,6 +65,10 @@ export default function alertTests({ getService }: FtrProviderContext) {
await esTestIndexToolOutput.destroy();
});
+ it('has a default action message', () => {
+ expect(DefaultActionMessage).to.be.ok();
+ });
+
// The tests below create two alerts, one that will fire, one that will
// never fire; the tests ensure the ones that should fire, do fire, and
// those that shouldn't fire, do not fire.
@@ -85,7 +92,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
const docs = await waitForDocs(2);
for (const doc of docs) {
const { group } = doc._source;
- const { name, value, title, message } = doc._source.params;
+ const { name, title, message } = doc._source.params;
expect(name).to.be('always fire');
expect(group).to.be('all documents');
@@ -93,9 +100,8 @@ export default function alertTests({ getService }: FtrProviderContext) {
// we'll check title and message in this test, but not subsequent ones
expect(title).to.be('alert always fire group all documents exceeded threshold');
- const expectedPrefix = `alert always fire group all documents value ${value} exceeded threshold count > -1 over`;
- const messagePrefix = message.substr(0, expectedPrefix.length);
- expect(messagePrefix).to.be(expectedPrefix);
+ const messagePattern = /alert always fire group all documents value \d+ exceeded threshold count > -1 over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
});
@@ -128,10 +134,13 @@ export default function alertTests({ getService }: FtrProviderContext) {
for (const doc of docs) {
const { group } = doc._source;
- const { name } = doc._source.params;
+ const { name, message } = doc._source.params;
expect(name).to.be('always fire');
if (group === 'group-0') inGroup0++;
+
+ const messagePattern = /alert always fire group group-\d value \d+ exceeded threshold count .+ over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
// there should be 2 docs in group-0, rando split between others
@@ -163,9 +172,12 @@ export default function alertTests({ getService }: FtrProviderContext) {
const docs = await waitForDocs(2);
for (const doc of docs) {
- const { name } = doc._source.params;
+ const { name, message } = doc._source.params;
expect(name).to.be('always fire');
+
+ const messagePattern = /alert always fire group all documents value \d+ exceeded threshold sum\(testedValue\) between 0,1000000 over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
});
@@ -195,9 +207,12 @@ export default function alertTests({ getService }: FtrProviderContext) {
const docs = await waitForDocs(4);
for (const doc of docs) {
- const { name } = doc._source.params;
+ const { name, message } = doc._source.params;
expect(name).to.be('always fire');
+
+ const messagePattern = /alert always fire group all documents value .+ exceeded threshold avg\(testedValue\) .+ 0 over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
});
@@ -232,10 +247,13 @@ export default function alertTests({ getService }: FtrProviderContext) {
for (const doc of docs) {
const { group } = doc._source;
- const { name } = doc._source.params;
+ const { name, message } = doc._source.params;
expect(name).to.be('always fire');
if (group === 'group-2') inGroup2++;
+
+ const messagePattern = /alert always fire group group-. value \d+ exceeded threshold max\(testedValue\) .* 0 over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
// there should be 2 docs in group-2, rando split between others
@@ -274,10 +292,13 @@ export default function alertTests({ getService }: FtrProviderContext) {
for (const doc of docs) {
const { group } = doc._source;
- const { name } = doc._source.params;
+ const { name, message } = doc._source.params;
expect(name).to.be('always fire');
if (group === 'group-0') inGroup0++;
+
+ const messagePattern = /alert always fire group group-. value \d+ exceeded threshold min\(testedValue\) .* 0 over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
// there should be 2 docs in group-0, rando split between others
@@ -329,7 +350,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
name: '{{{alertName}}}',
value: '{{{context.value}}}',
title: '{{{context.title}}}',
- message: '{{{context.message}}}',
+ message: DefaultActionMessage,
},
date: '{{{context.date}}}',
// TODO: I wanted to write the alert value here, but how?
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts
index ee0de582a9bff..0f6da936f8644 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts
@@ -79,10 +79,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)');
const createdConnectorToastTitle = await pageObjects.common.closeToast();
expect(createdConnectorToastTitle).to.eql(`Created '${slackConnectorName}'`);
+ const messageTextArea = await find.byCssSelector('[data-test-subj="messageTextArea"]');
+ expect(await messageTextArea.getAttribute('value')).to.eql(
+ 'alert {{alertName}} group {{context.group}} value {{context.value}} exceeded threshold {{context.function}} over {{params.timeWindowSize}}{{params.timeWindowUnit}} on {{context.date}}'
+ );
await testSubjects.setValue('messageTextArea', 'test message ');
await testSubjects.click('messageAddVariableButton');
await testSubjects.click('variableMenuButton-0');
- const messageTextArea = await find.byCssSelector('[data-test-subj="messageTextArea"]');
expect(await messageTextArea.getAttribute('value')).to.eql('test message {{alertId}}');
await messageTextArea.type(' some additional text ');