')
+ .description('Get the help for a specific command')
+ .action(function (cmdName) {
+ const cmd = Object.values(program.commands).find((command) => command._name === cmdName);
+ if (!cmd) return program.error(`unknown command ${cmdName}`);
+ cmd.help();
+ });
+
+program.command('*', null, { noHelp: true }).action(function (cmd) {
+ program.error(`unknown command ${cmd}`);
+});
+
+// check for no command name
+const subCommand = argv[2] && !String(argv[2][0]).match(/^-|^\.|\//);
+if (!subCommand) {
+ program.defaultHelp();
+}
+
+program.parse(process.argv);
diff --git a/src/cli_encryption_keys/dev.js b/src/cli_encryption_keys/dev.js
new file mode 100644
index 0000000000000..544374f6107a8
--- /dev/null
+++ b/src/cli_encryption_keys/dev.js
@@ -0,0 +1,21 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+require('../setup_node_env');
+require('./cli_encryption_keys');
diff --git a/src/cli_encryption_keys/dist.js b/src/cli_encryption_keys/dist.js
new file mode 100644
index 0000000000000..1c0ed01e65506
--- /dev/null
+++ b/src/cli_encryption_keys/dist.js
@@ -0,0 +1,21 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+require('../setup_node_env/dist');
+require('./cli_encryption_keys');
diff --git a/src/cli_encryption_keys/encryption_config.js b/src/cli_encryption_keys/encryption_config.js
new file mode 100644
index 0000000000000..f5cf4ba0b037e
--- /dev/null
+++ b/src/cli_encryption_keys/encryption_config.js
@@ -0,0 +1,86 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import crypto from 'crypto';
+import { join } from 'path';
+import { get } from 'lodash';
+import { readFileSync } from 'fs';
+import { safeLoad } from 'js-yaml';
+
+import { getConfigDirectory } from '@kbn/utils';
+
+export class EncryptionConfig {
+ #config = safeLoad(readFileSync(join(getConfigDirectory(), 'kibana.yml')));
+ #encryptionKeyPaths = [
+ 'xpack.encryptedSavedObjects.encryptionKey',
+ 'xpack.reporting.encryptionKey',
+ 'xpack.security.encryptionKey',
+ ];
+ #encryptionMeta = {
+ 'xpack.encryptedSavedObjects.encryptionKey': {
+ docs:
+ 'https://www.elastic.co/guide/en/kibana/current/xpack-security-secure-saved-objects.html#xpack-security-secure-saved-objects',
+ description: 'Used to encrypt stored objects such as dashboards and visualizations',
+ },
+ 'xpack.reporting.encryptionKey': {
+ docs:
+ 'https://www.elastic.co/guide/en/kibana/current/reporting-settings-kb.html#general-reporting-settings',
+ description: 'Used to encrypt saved reports',
+ },
+ 'xpack.security.encryptionKey': {
+ docs:
+ 'https://www.elastic.co/guide/en/kibana/current/security-settings-kb.html#security-session-and-cookie-settings',
+ description: 'Used to encrypt session information',
+ },
+ };
+
+ _getEncryptionKey(key) {
+ return get(this.#config, key);
+ }
+
+ _hasEncryptionKey(key) {
+ return !!get(this.#config, key);
+ }
+
+ _generateEncryptionKey() {
+ return crypto.randomBytes(16).toString('hex');
+ }
+
+ docs({ comment } = {}) {
+ const commentString = comment ? '#' : '';
+ let docs = '';
+ this.#encryptionKeyPaths.forEach((key) => {
+ docs += `${commentString}${key}
+ ${commentString}${this.#encryptionMeta[key].description}
+ ${commentString}${this.#encryptionMeta[key].docs}
+\n`;
+ });
+ return docs;
+ }
+
+ generate({ force = false }) {
+ const output = {};
+ this.#encryptionKeyPaths.forEach((key) => {
+ if (force || !this._hasEncryptionKey(key)) {
+ output[key] = this._generateEncryptionKey();
+ }
+ });
+ return output;
+ }
+}
diff --git a/src/cli_encryption_keys/encryption_config.test.js b/src/cli_encryption_keys/encryption_config.test.js
new file mode 100644
index 0000000000000..60220d0270b4e
--- /dev/null
+++ b/src/cli_encryption_keys/encryption_config.test.js
@@ -0,0 +1,83 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { EncryptionConfig } from './encryption_config';
+import crypto from 'crypto';
+import fs from 'fs';
+
+describe('encryption key configuration', () => {
+ let encryptionConfig = null;
+
+ beforeEach(() => {
+ jest.spyOn(fs, 'readFileSync').mockReturnValue('xpack.security.encryptionKey: foo');
+ jest.spyOn(crypto, 'randomBytes').mockReturnValue('random-key');
+ encryptionConfig = new EncryptionConfig();
+ });
+ it('should be able to check for encryption keys', () => {
+ expect(encryptionConfig._hasEncryptionKey('xpack.reporting.encryptionKey')).toEqual(false);
+ expect(encryptionConfig._hasEncryptionKey('xpack.security.encryptionKey')).toEqual(true);
+ });
+
+ it('should be able to get encryption keys', () => {
+ expect(encryptionConfig._getEncryptionKey('xpack.reporting.encryptionKey')).toBeUndefined();
+ expect(encryptionConfig._getEncryptionKey('xpack.security.encryptionKey')).toEqual('foo');
+ });
+
+ it('should generate a key', () => {
+ expect(encryptionConfig._generateEncryptionKey()).toEqual('random-key');
+ });
+
+ it('should only generate unset keys', () => {
+ const output = encryptionConfig.generate({ force: false });
+ expect(output['xpack.security.encryptionKey']).toEqual(undefined);
+ expect(output['xpack.reporting.encryptionKey']).toEqual('random-key');
+ });
+
+ it('should regenerate all keys if the force flag is set', () => {
+ const output = encryptionConfig.generate({ force: true });
+ expect(output['xpack.security.encryptionKey']).toEqual('random-key');
+ expect(output['xpack.reporting.encryptionKey']).toEqual('random-key');
+ expect(output['xpack.encryptedSavedObjects.encryptionKey']).toEqual('random-key');
+ });
+
+ it('should set encryptedObjects and reporting with a default configuration', () => {
+ const output = encryptionConfig.generate({});
+ expect(output['xpack.security.encryptionKey']).toBeUndefined();
+ expect(output['xpack.encryptedSavedObjects.encryptionKey']).toEqual('random-key');
+ expect(output['xpack.reporting.encryptionKey']).toEqual('random-key');
+ });
+});
diff --git a/src/cli_encryption_keys/generate.js b/src/cli_encryption_keys/generate.js
new file mode 100644
index 0000000000000..a47fa6add6e3b
--- /dev/null
+++ b/src/cli_encryption_keys/generate.js
@@ -0,0 +1,59 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { safeDump } from 'js-yaml';
+import { isEmpty } from 'lodash';
+import { interactive } from './interactive';
+import { Logger } from '../cli_plugin/lib/logger';
+
+export async function generate(encryptionConfig, command) {
+ const logger = new Logger();
+ const keys = encryptionConfig.generate({ force: command.force });
+ if (isEmpty(keys)) {
+ logger.log('No keys to write. Use the --force flag to generate new keys.');
+ } else {
+ if (!command.quiet) {
+ logger.log('## Kibana Encryption Key Generation Utility\n');
+ logger.log(
+ `The 'generate' command guides you through the process of setting encryption keys for:\n`
+ );
+ logger.log(encryptionConfig.docs());
+ logger.log(
+ 'Already defined settings are ignored and can be regenerated using the --force flag. Check the documentation links for instructions on how to rotate encryption keys.'
+ );
+ logger.log('Definitions should be set in the kibana.yml used configure Kibana.\n');
+ }
+ if (command.interactive) {
+ await interactive(keys, encryptionConfig.docs({ comment: true }), logger);
+ } else {
+ if (!command.quiet) logger.log('Settings:');
+ logger.log(safeDump(keys));
+ }
+ }
+}
+
+export function generateCli(program, encryptionConfig) {
+ program
+ .command('generate')
+ .description('Generates encryption keys')
+ .option('-i, --interactive', 'interactive output')
+ .option('-q, --quiet', 'do not include instructions')
+ .option('-f, --force', 'generate new keys for all settings')
+ .action(generate.bind(null, encryptionConfig));
+}
diff --git a/src/cli_encryption_keys/generate.test.js b/src/cli_encryption_keys/generate.test.js
new file mode 100644
index 0000000000000..65fb8ebc028f1
--- /dev/null
+++ b/src/cli_encryption_keys/generate.test.js
@@ -0,0 +1,56 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { EncryptionConfig } from './encryption_config';
+import { generate } from './generate';
+
+import { Logger } from '../cli_plugin/lib/logger';
+
+describe('encryption key generation', () => {
+ const encryptionConfig = new EncryptionConfig();
+ beforeEach(() => {
+ Logger.prototype.log = jest.fn();
+ });
+
+ it('should generate a new encryption config', () => {
+ const command = {
+ force: false,
+ interactive: false,
+ quiet: false,
+ };
+ generate(encryptionConfig, command);
+ const keys = Logger.prototype.log.mock.calls[6][0];
+ expect(keys.search('xpack.encryptedSavedObjects.encryptionKey')).toBeGreaterThanOrEqual(0);
+ expect(keys.search('xpack.reporting.encryptionKey')).toBeGreaterThanOrEqual(0);
+ expect(keys.search('xpack.security.encryptionKey')).toBeGreaterThanOrEqual(0);
+ expect(keys.search('foo.bar')).toEqual(-1);
+ });
+
+ it('should only output keys if the quiet flag is set', () => {
+ generate(encryptionConfig, { quiet: true });
+ const keys = Logger.prototype.log.mock.calls[0][0];
+ const nextLog = Logger.prototype.log.mock.calls[1];
+ expect(keys.search('xpack.encryptedSavedObjects.encryptionKey')).toBeGreaterThanOrEqual(0);
+ expect(nextLog).toEqual(undefined);
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+});
diff --git a/src/cli_encryption_keys/interactive.js b/src/cli_encryption_keys/interactive.js
new file mode 100644
index 0000000000000..c5d716077672d
--- /dev/null
+++ b/src/cli_encryption_keys/interactive.js
@@ -0,0 +1,55 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { writeFileSync } from 'fs';
+import { join } from 'path';
+import { confirm, question } from '../cli_keystore/utils';
+import { getConfigDirectory } from '@kbn/utils';
+import { safeDump } from 'js-yaml';
+
+export async function interactive(keys, docs, logger) {
+ const settings = Object.keys(keys);
+ logger.log(
+ 'This tool will ask you a number of questions in order to generate the right set of keys for your needs.\n'
+ );
+ const setKeys = {};
+ for (const setting of settings) {
+ const include = await confirm(`Set ${setting}?`);
+ if (include) setKeys[setting] = keys[setting];
+ }
+ const count = Object.keys(setKeys).length;
+ const plural = count > 1 ? 's were' : ' was';
+ logger.log('');
+ if (!count) return logger.log('No keys were generated');
+ logger.log(`The following key${plural} generated:`);
+ logger.log(Object.keys(setKeys).join('\n'));
+ logger.log('');
+ const write = await confirm('Save generated keys to a sample Kibana configuration file?');
+ if (write) {
+ const defaultSaveLocation = join(getConfigDirectory(), 'kibana.sample.yml');
+ const promptedSaveLocation = await question(
+ `What filename should be used for the sample Kibana config file? [${defaultSaveLocation}])`
+ );
+ const saveLocation = promptedSaveLocation || defaultSaveLocation;
+ writeFileSync(saveLocation, docs + safeDump(setKeys));
+ logger.log(`Wrote configuration to ${saveLocation}`);
+ } else {
+ logger.log('\nSettings:');
+ logger.log(safeDump(setKeys));
+ }
+}
diff --git a/src/cli_encryption_keys/interactive.test.js b/src/cli_encryption_keys/interactive.test.js
new file mode 100644
index 0000000000000..cba722d85c545
--- /dev/null
+++ b/src/cli_encryption_keys/interactive.test.js
@@ -0,0 +1,69 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { EncryptionConfig } from './encryption_config';
+import { generate } from './generate';
+
+import { Logger } from '../cli_plugin/lib/logger';
+import * as prompt from '../cli_keystore/utils/prompt';
+import fs from 'fs';
+import crypto from 'crypto';
+
+describe('encryption key generation interactive', () => {
+ const encryptionConfig = new EncryptionConfig();
+ beforeEach(() => {
+ Logger.prototype.log = jest.fn();
+ });
+
+ it('should prompt the user to write keys if the interactive flag is set', async () => {
+ jest
+ .spyOn(prompt, 'confirm')
+ .mockResolvedValueOnce(true)
+ .mockResolvedValueOnce(true)
+ .mockResolvedValueOnce(true)
+ .mockResolvedValueOnce(false);
+ jest.spyOn(prompt, 'question');
+
+ await generate(encryptionConfig, { interactive: true });
+ expect(prompt.confirm.mock.calls).toEqual([
+ ['Set xpack.encryptedSavedObjects.encryptionKey?'],
+ ['Set xpack.reporting.encryptionKey?'],
+ ['Set xpack.security.encryptionKey?'],
+ ['Save generated keys to a sample Kibana configuration file?'],
+ ]);
+ expect(prompt.question).not.toHaveBeenCalled();
+ });
+
+ it('should write to disk partial keys', async () => {
+ jest
+ .spyOn(prompt, 'confirm')
+ .mockResolvedValueOnce(true)
+ .mockResolvedValueOnce(false)
+ .mockResolvedValueOnce(false)
+ .mockResolvedValueOnce(true);
+ jest.spyOn(prompt, 'question').mockResolvedValue('/foo/bar');
+ jest.spyOn(crypto, 'randomBytes').mockReturnValue('random-key');
+ fs.writeFileSync = jest.fn();
+ await generate(encryptionConfig, { interactive: true });
+ expect(fs.writeFileSync.mock.calls).toMatchSnapshot();
+ });
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+});
diff --git a/src/core/public/utils/crypto/sha256.ts b/src/core/public/utils/crypto/sha256.ts
index eaa057d604689..13e0d405a706b 100644
--- a/src/core/public/utils/crypto/sha256.ts
+++ b/src/core/public/utils/crypto/sha256.ts
@@ -130,7 +130,7 @@ type BufferEncoding =
| 'binary'
| 'hex';
-/* eslint-disable no-bitwise, no-shadow */
+/* eslint-disable no-bitwise, @typescript-eslint/no-shadow */
export class Sha256 {
private _a: number;
private _b: number;
diff --git a/src/core/server/http/router/validator/validator.ts b/src/core/server/http/router/validator/validator.ts
index babca87495a4e..be7781fdacbe0 100644
--- a/src/core/server/http/router/validator/validator.ts
+++ b/src/core/server/http/router/validator/validator.ts
@@ -143,8 +143,8 @@ export type RouteValidatorFullConfig = RouteValidatorConfig
&
* @internal
*/
export class RouteValidator
{
- public static from
(
- opts: RouteValidator
| RouteValidatorFullConfig
+ public static from<_P = {}, _Q = {}, _B = {}>(
+ opts: RouteValidator<_P, _Q, _B> | RouteValidatorFullConfig<_P, _Q, _B>
) {
if (opts instanceof RouteValidator) {
return opts;
diff --git a/src/dev/build/tasks/bin/scripts/kibana-encryption-keys b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys
new file mode 100755
index 0000000000000..5df19558214d3
--- /dev/null
+++ b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys
@@ -0,0 +1,29 @@
+#!/bin/sh
+SCRIPT=$0
+
+# SCRIPT may be an arbitrarily deep series of symlinks. Loop until we have the concrete path.
+while [ -h "$SCRIPT" ] ; do
+ ls=$(ls -ld "$SCRIPT")
+ # Drop everything prior to ->
+ link=$(expr "$ls" : '.*-> \(.*\)$')
+ if expr "$link" : '/.*' > /dev/null; then
+ SCRIPT="$link"
+ else
+ SCRIPT=$(dirname "$SCRIPT")/"$link"
+ fi
+done
+
+DIR="$(dirname "${SCRIPT}")/.."
+CONFIG_DIR=${KBN_PATH_CONF:-"$DIR/config"}
+NODE="${DIR}/node/bin/node"
+test -x "$NODE"
+if [ ! -x "$NODE" ]; then
+ echo "unable to find usable node.js executable."
+ exit 1
+fi
+
+if [ -f "${CONFIG_DIR}/node.options" ]; then
+ KBN_NODE_OPTS="$(grep -v ^# < ${CONFIG_DIR}/node.options | xargs)"
+fi
+
+NODE_OPTIONS="$KBN_NODE_OPTS $NODE_OPTIONS" "${NODE}" "${DIR}/src/cli_encryption_keys/dist" "$@"
diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js
index 2890ec95429b5..fabeda4171a37 100644
--- a/src/dev/jest/config.js
+++ b/src/dev/jest/config.js
@@ -27,6 +27,7 @@ export default {
'/src/legacy/server',
'/src/cli',
'/src/cli_keystore',
+ '/src/cli_encryption_keys',
'/src/cli_plugin',
'/packages/kbn-test/target/functional_test_runner',
'/src/dev',
diff --git a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap
index afaa2d00d8cfd..3e09fa449a1aa 100644
--- a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap
+++ b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap
@@ -47,7 +47,7 @@ Object {
],
},
"count": 1,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"text",
],
diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts
index 850c5a312fda1..4dd2d29f38e9f 100644
--- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts
+++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts
@@ -68,12 +68,12 @@ export class IndexPatternField implements IFieldType {
this.spec.lang = lang;
}
- public get customName() {
- return this.spec.customName;
+ public get customLabel() {
+ return this.spec.customLabel;
}
- public set customName(label) {
- this.spec.customName = label;
+ public set customLabel(customLabel) {
+ this.spec.customLabel = customLabel;
}
/**
@@ -93,8 +93,8 @@ export class IndexPatternField implements IFieldType {
}
public get displayName(): string {
- return this.spec.customName
- ? this.spec.customName
+ return this.spec.customLabel
+ ? this.spec.customLabel
: this.spec.shortDotsEnable
? shortenDottedString(this.spec.name)
: this.spec.name;
@@ -163,7 +163,7 @@ export class IndexPatternField implements IFieldType {
aggregatable: this.aggregatable,
readFromDocValues: this.readFromDocValues,
subType: this.subType,
- customName: this.customName,
+ customLabel: this.customLabel,
};
}
@@ -186,7 +186,7 @@ export class IndexPatternField implements IFieldType {
readFromDocValues: this.readFromDocValues,
subType: this.subType,
format: getFormatterForField ? getFormatterForField(this).toJSON() : undefined,
- customName: this.customName,
+ customLabel: this.customLabel,
shortDotsEnable: this.spec.shortDotsEnable,
};
}
diff --git a/src/plugins/data/common/index_patterns/fields/types.ts b/src/plugins/data/common/index_patterns/fields/types.ts
index 86c22b0116ead..1c70a2e884025 100644
--- a/src/plugins/data/common/index_patterns/fields/types.ts
+++ b/src/plugins/data/common/index_patterns/fields/types.ts
@@ -37,7 +37,7 @@ export interface IFieldType {
scripted?: boolean;
subType?: IFieldSubType;
displayName?: string;
- customName?: string;
+ customLabel?: string;
format?: any;
toSpec?: (options?: { getFormatterForField?: IndexPattern['getFormatterForField'] }) => FieldSpec;
}
diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap
index 2741322acec0f..e2bdb0009c20a 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap
+++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap
@@ -9,7 +9,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"keyword",
],
@@ -33,7 +33,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 30,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"date",
],
@@ -57,7 +57,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"_id",
],
@@ -81,7 +81,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"_source",
],
@@ -105,7 +105,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"_type",
],
@@ -129,7 +129,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"geo_shape",
],
@@ -153,7 +153,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 10,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"long",
],
@@ -177,7 +177,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"conflict",
],
@@ -201,7 +201,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"text",
],
@@ -225,7 +225,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"keyword",
],
@@ -253,7 +253,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"geo_point",
],
@@ -277,7 +277,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"keyword",
],
@@ -301,7 +301,7 @@ Object {
"aggregatable": false,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"murmur3",
],
@@ -325,7 +325,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"ip",
],
@@ -349,7 +349,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"text",
],
@@ -373,7 +373,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"keyword",
],
@@ -401,7 +401,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"text",
],
@@ -425,7 +425,7 @@ Object {
"aggregatable": false,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"text",
],
@@ -449,7 +449,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"integer",
],
@@ -473,7 +473,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"geo_point",
],
@@ -497,7 +497,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"attachment",
],
@@ -521,7 +521,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"date",
],
@@ -545,7 +545,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"murmur3",
],
@@ -569,7 +569,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"long",
],
@@ -593,7 +593,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"text",
],
@@ -617,7 +617,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 20,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"boolean",
],
@@ -641,7 +641,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 30,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"date",
],
@@ -665,7 +665,7 @@ Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
- "customName": undefined,
+ "customLabel": undefined,
"esTypes": Array [
"date",
],
diff --git a/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts b/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts
index a3653bb529fa3..19fe7c7c26c79 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts
@@ -20,8 +20,8 @@
import { IndexPattern } from './index_pattern';
export interface PatternCache {
- get: (id: string) => IndexPattern;
- set: (id: string, value: IndexPattern) => IndexPattern;
+ get: (id: string) => Promise | undefined;
+ set: (id: string, value: Promise) => Promise;
clear: (id: string) => void;
clearAll: () => void;
}
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
index c3a0c98745e21..47ad5860801bc 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
@@ -135,8 +135,8 @@ export class IndexPattern implements IIndexPattern {
const newFieldAttrs = { ...this.fieldAttrs };
this.fields.forEach((field) => {
- if (field.customName) {
- newFieldAttrs[field.name] = { customName: field.customName };
+ if (field.customLabel) {
+ newFieldAttrs[field.name] = { customLabel: field.customLabel };
} else {
delete newFieldAttrs[field.name];
}
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts
index b22437ebbdb4e..bf227615f76a1 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts
@@ -40,6 +40,7 @@ function setDocsourcePayload(id: string | null, providedPayload: any) {
describe('IndexPatterns', () => {
let indexPatterns: IndexPatternsService;
let savedObjectsClient: SavedObjectsClientCommon;
+ let SOClientGetDelay = 0;
beforeEach(() => {
const indexPatternObj = { id: 'id', version: 'a', attributes: { title: 'title' } };
@@ -49,11 +50,14 @@ describe('IndexPatterns', () => {
);
savedObjectsClient.delete = jest.fn(() => Promise.resolve({}) as Promise);
savedObjectsClient.create = jest.fn();
- savedObjectsClient.get = jest.fn().mockImplementation(async (type, id) => ({
- id: object.id,
- version: object.version,
- attributes: object.attributes,
- }));
+ savedObjectsClient.get = jest.fn().mockImplementation(async (type, id) => {
+ await new Promise((resolve) => setTimeout(resolve, SOClientGetDelay));
+ return {
+ id: object.id,
+ version: object.version,
+ attributes: object.attributes,
+ };
+ });
savedObjectsClient.update = jest
.fn()
.mockImplementation(async (type, id, body, { version }) => {
@@ -87,6 +91,7 @@ describe('IndexPatterns', () => {
});
test('does cache gets for the same id', async () => {
+ SOClientGetDelay = 1000;
const id = '1';
setDocsourcePayload(id, {
id: 'foo',
@@ -96,10 +101,17 @@ describe('IndexPatterns', () => {
},
});
- const indexPattern = await indexPatterns.get(id);
+ // make two requests before first can complete
+ const indexPatternPromise = indexPatterns.get(id);
+ indexPatterns.get(id);
- expect(indexPattern).toBeDefined();
- expect(indexPattern).toBe(await indexPatterns.get(id));
+ indexPatternPromise.then((indexPattern) => {
+ expect(savedObjectsClient.get).toBeCalledTimes(1);
+ expect(indexPattern).toBeDefined();
+ });
+
+ expect(await indexPatternPromise).toBe(await indexPatterns.get(id));
+ SOClientGetDelay = 0;
});
test('savedObjectCache pre-fetches only title', async () => {
@@ -211,4 +223,25 @@ describe('IndexPatterns', () => {
expect(indexPatterns.savedObjectToSpec(savedObject)).toMatchSnapshot();
});
+
+ test('failed requests are not cached', async () => {
+ savedObjectsClient.get = jest
+ .fn()
+ .mockImplementation(async (type, id) => {
+ return {
+ id: object.id,
+ version: object.version,
+ attributes: object.attributes,
+ };
+ })
+ .mockRejectedValueOnce({});
+
+ const id = '1';
+
+ // failed request!
+ expect(indexPatterns.get(id)).rejects.toBeDefined();
+
+ // successful subsequent request
+ expect(async () => await indexPatterns.get(id)).toBeDefined();
+ });
});
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
index 4f91079c1e139..82c8cf4abc5ac 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
@@ -309,7 +309,7 @@ export class IndexPatternsService {
*/
fieldArrayToMap = (fields: FieldSpec[], fieldAttrs?: FieldAttrs) =>
fields.reduce((collector, field) => {
- collector[field.name] = { ...field, customName: fieldAttrs?.[field.name]?.customName };
+ collector[field.name] = { ...field, customLabel: fieldAttrs?.[field.name]?.customLabel };
return collector;
}, {});
@@ -356,17 +356,7 @@ export class IndexPatternsService {
};
};
- /**
- * Get an index pattern by id. Cache optimized
- * @param id
- */
-
- get = async (id: string): Promise => {
- const cache = indexPatternCache.get(id);
- if (cache) {
- return cache;
- }
-
+ private getSavedObjectAndInit = async (id: string): Promise => {
const savedObject = await this.savedObjectsClient.get(
savedObjectType,
id
@@ -422,7 +412,6 @@ export class IndexPatternsService {
: {};
const indexPattern = await this.create(spec, true);
- indexPatternCache.set(id, indexPattern);
if (isSaveRequired) {
try {
this.updateSavedObject(indexPattern);
@@ -444,6 +433,23 @@ export class IndexPatternsService {
return indexPattern;
};
+ /**
+ * Get an index pattern by id. Cache optimized
+ * @param id
+ */
+
+ get = async (id: string): Promise => {
+ const indexPatternPromise =
+ indexPatternCache.get(id) || indexPatternCache.set(id, this.getSavedObjectAndInit(id));
+
+ // don't cache failed requests
+ indexPatternPromise.catch(() => {
+ indexPatternCache.clear(id);
+ });
+
+ return indexPatternPromise;
+ };
+
/**
* Create a new index pattern instance
* @param spec
@@ -502,7 +508,7 @@ export class IndexPatternsService {
id: indexPattern.id,
});
indexPattern.id = response.id;
- indexPatternCache.set(indexPattern.id, indexPattern);
+ indexPatternCache.set(indexPattern.id, Promise.resolve(indexPattern));
return indexPattern;
}
diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts
index 22c400562f6d4..28b077f4bfdf3 100644
--- a/src/plugins/data/common/index_patterns/types.ts
+++ b/src/plugins/data/common/index_patterns/types.ts
@@ -52,7 +52,7 @@ export interface IndexPatternAttributes {
}
export interface FieldAttrs {
- [key: string]: { customName: string };
+ [key: string]: { customLabel: string };
}
export type OnNotification = (toastInputFields: ToastInputFields) => void;
@@ -169,7 +169,7 @@ export interface FieldSpec {
readFromDocValues?: boolean;
subType?: IFieldSubType;
indexed?: boolean;
- customName?: string;
+ customLabel?: string;
// not persisted
shortDotsEnable?: boolean;
}
diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts
index 3ffac0c12eb22..4f4a593764b1e 100644
--- a/src/plugins/data/common/search/aggs/agg_type.ts
+++ b/src/plugins/data/common/search/aggs/agg_type.ts
@@ -54,7 +54,7 @@ export interface AggTypeConfig<
aggConfigs: IAggConfigs,
aggConfig: TAggConfig,
searchSource: ISearchSource,
- inspectorRequestAdapter: RequestAdapter,
+ inspectorRequestAdapter?: RequestAdapter,
abortSignal?: AbortSignal
) => Promise;
getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat;
@@ -189,7 +189,7 @@ export class AggType<
aggConfigs: IAggConfigs,
aggConfig: TAggConfig,
searchSource: ISearchSource,
- inspectorRequestAdapter: RequestAdapter,
+ inspectorRequestAdapter?: RequestAdapter,
abortSignal?: AbortSignal
) => Promise;
/**
diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts
index 3d543e6c5f574..ac65e7fa813b3 100644
--- a/src/plugins/data/common/search/aggs/buckets/terms.ts
+++ b/src/plugins/data/common/search/aggs/buckets/terms.ts
@@ -19,6 +19,7 @@
import { noop } from 'lodash';
import { i18n } from '@kbn/i18n';
+import type { RequestAdapter } from 'src/plugins/inspector/common';
import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
@@ -111,27 +112,32 @@ export const getTermsBucketAgg = () =>
nestedSearchSource.setField('aggs', filterAgg);
- const request = inspectorRequestAdapter.start(
- i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', {
- defaultMessage: 'Other bucket',
- }),
- {
- description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', {
- defaultMessage:
- 'This request counts the number of documents that fall ' +
- 'outside the criterion of the data buckets.',
+ let request: ReturnType | undefined;
+ if (inspectorRequestAdapter) {
+ request = inspectorRequestAdapter.start(
+ i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', {
+ defaultMessage: 'Other bucket',
}),
- }
- );
- nestedSearchSource.getSearchRequestBody().then((body) => {
- request.json(body);
- });
- request.stats(getRequestInspectorStats(nestedSearchSource));
+ {
+ description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', {
+ defaultMessage:
+ 'This request counts the number of documents that fall ' +
+ 'outside the criterion of the data buckets.',
+ }),
+ }
+ );
+ nestedSearchSource.getSearchRequestBody().then((body) => {
+ request!.json(body);
+ });
+ request.stats(getRequestInspectorStats(nestedSearchSource));
+ }
const response = await nestedSearchSource.fetch({ abortSignal });
- request
- .stats(getResponseInspectorStats(response, nestedSearchSource))
- .ok({ json: response });
+ if (request) {
+ request
+ .stats(getResponseInspectorStats(response, nestedSearchSource))
+ .ok({ json: response });
+ }
resp = mergeOtherBucketAggResponse(aggConfigs, resp, response, aggConfig, filterAgg());
}
if (aggConfig.params.missingBucket) {
diff --git a/src/plugins/data/common/search/session/index.ts b/src/plugins/data/common/search/session/index.ts
index d8f7b5091eb8f..0feb43f8f1d4b 100644
--- a/src/plugins/data/common/search/session/index.ts
+++ b/src/plugins/data/common/search/session/index.ts
@@ -17,4 +17,5 @@
* under the License.
*/
+export * from './status';
export * from './types';
diff --git a/src/plugins/data/common/search/session/mocks.ts b/src/plugins/data/common/search/session/mocks.ts
index 370faaa640c56..4604e15e4e93b 100644
--- a/src/plugins/data/common/search/session/mocks.ts
+++ b/src/plugins/data/common/search/session/mocks.ts
@@ -27,5 +27,12 @@ export function getSessionServiceMock(): jest.Mocked {
restore: jest.fn(),
getSessionId: jest.fn(),
getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()),
+ isStored: jest.fn(),
+ isRestore: jest.fn(),
+ save: jest.fn(),
+ get: jest.fn(),
+ find: jest.fn(),
+ update: jest.fn(),
+ delete: jest.fn(),
};
}
diff --git a/src/plugins/data/common/search/session/status.ts b/src/plugins/data/common/search/session/status.ts
new file mode 100644
index 0000000000000..1f6b6eb3084bb
--- /dev/null
+++ b/src/plugins/data/common/search/session/status.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export enum BackgroundSessionStatus {
+ IN_PROGRESS = 'in_progress',
+ ERROR = 'error',
+ COMPLETE = 'complete',
+ CANCELLED = 'cancelled',
+ EXPIRED = 'expired',
+}
diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/common/search/session/types.ts
index 6660b8395547f..d1ab22057695a 100644
--- a/src/plugins/data/common/search/session/types.ts
+++ b/src/plugins/data/common/search/session/types.ts
@@ -18,6 +18,7 @@
*/
import { Observable } from 'rxjs';
+import type { SavedObject, SavedObjectsFindResponse } from 'kibana/server';
export interface ISessionService {
/**
@@ -30,6 +31,17 @@ export interface ISessionService {
* @returns `Observable`
*/
getSession$: () => Observable;
+
+ /**
+ * Whether the active session is already saved (i.e. sent to background)
+ */
+ isStored: () => boolean;
+
+ /**
+ * Whether the active session is restored (i.e. reusing previous search IDs)
+ */
+ isRestore: () => boolean;
+
/**
* Starts a new session
*/
@@ -38,10 +50,58 @@ export interface ISessionService {
/**
* Restores existing session
*/
- restore: (sessionId: string) => void;
+ restore: (sessionId: string) => Promise>;
/**
* Clears the active session.
*/
clear: () => void;
+
+ /**
+ * Saves a session
+ */
+ save: (name: string, url: string) => Promise>;
+
+ /**
+ * Gets a saved session
+ */
+ get: (sessionId: string) => Promise>;
+
+ /**
+ * Gets a list of saved sessions
+ */
+ find: (
+ options: SearchSessionFindOptions
+ ) => Promise>;
+
+ /**
+ * Updates a session
+ */
+ update: (
+ sessionId: string,
+ attributes: Partial
+ ) => Promise;
+
+ /**
+ * Deletes a session
+ */
+ delete: (sessionId: string) => Promise;
+}
+
+export interface BackgroundSessionSavedObjectAttributes {
+ name: string;
+ created: string;
+ expires: string;
+ status: string;
+ initialState: Record;
+ restoreState: Record;
+ idMapping: Record;
+}
+
+export interface SearchSessionFindOptions {
+ page?: number;
+ perPage?: number;
+ sortField?: string;
+ sortOrder?: string;
+ filter?: string;
}
diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts
index 7451edf5e2fa3..695ee34d3b468 100644
--- a/src/plugins/data/common/search/types.ts
+++ b/src/plugins/data/common/search/types.ts
@@ -92,4 +92,15 @@ export interface ISearchOptions {
* A session ID, grouping multiple search requests into a single session.
*/
sessionId?: string;
+
+ /**
+ * Whether the session is already saved (i.e. sent to background)
+ */
+ isStored?: boolean;
+
+ /**
+ * Whether the session is restored (i.e. search requests should re-use the stored search IDs,
+ * rather than starting from scratch)
+ */
+ isRestore?: boolean;
}
diff --git a/src/plugins/data/common/utils/index.ts b/src/plugins/data/common/utils/index.ts
index 8b8686c51b9c1..4b602cb963a8f 100644
--- a/src/plugins/data/common/utils/index.ts
+++ b/src/plugins/data/common/utils/index.ts
@@ -19,3 +19,4 @@
/** @internal */
export { shortenDottedString } from './shorten_dotted_string';
+export { tapFirst } from './tap_first';
diff --git a/src/plugins/data/common/utils/tap_first.test.ts b/src/plugins/data/common/utils/tap_first.test.ts
new file mode 100644
index 0000000000000..033ae59f8c715
--- /dev/null
+++ b/src/plugins/data/common/utils/tap_first.test.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { of } from 'rxjs';
+import { tapFirst } from './tap_first';
+
+describe('tapFirst', () => {
+ it('should tap the first and only the first', () => {
+ const fn = jest.fn();
+ of(1, 2, 3).pipe(tapFirst(fn)).subscribe();
+ expect(fn).toBeCalledTimes(1);
+ expect(fn).lastCalledWith(1);
+ });
+});
diff --git a/src/plugins/data/common/utils/tap_first.ts b/src/plugins/data/common/utils/tap_first.ts
new file mode 100644
index 0000000000000..2c783a3ef87f0
--- /dev/null
+++ b/src/plugins/data/common/utils/tap_first.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { pipe } from 'rxjs';
+import { tap } from 'rxjs/operators';
+
+export function tapFirst(next: (x: T) => void) {
+ let isFirst = true;
+ return pipe(
+ tap((x: T) => {
+ if (isFirst) next(x);
+ isFirst = false;
+ })
+ );
+}
diff --git a/src/plugins/data/public/autocomplete/autocomplete_service.ts b/src/plugins/data/public/autocomplete/autocomplete_service.ts
index 2136a405baad6..5e9aede0760fe 100644
--- a/src/plugins/data/public/autocomplete/autocomplete_service.ts
+++ b/src/plugins/data/public/autocomplete/autocomplete_service.ts
@@ -18,6 +18,7 @@
*/
import { CoreSetup, PluginInitializerContext } from 'src/core/public';
+import { TimefilterSetup } from '../query';
import { QuerySuggestionGetFn } from './providers/query_suggestion_provider';
import {
getEmptyValueSuggestions,
@@ -57,9 +58,9 @@ export class AutocompleteService {
private hasQuerySuggestions = (language: string) => this.querySuggestionProviders.has(language);
/** @public **/
- public setup(core: CoreSetup) {
+ public setup(core: CoreSetup, { timefilter }: { timefilter: TimefilterSetup }) {
this.getValueSuggestions = this.autocompleteConfig.valueSuggestions.enabled
- ? setupValueSuggestionProvider(core)
+ ? setupValueSuggestionProvider(core, { timefilter })
: getEmptyValueSuggestions;
return {
diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts
index 0ef5b7db958e4..4e1745ffcabb2 100644
--- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts
+++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts
@@ -18,29 +18,10 @@
*/
import { stubIndexPattern, stubFields } from '../../stubs';
+import { TimefilterSetup } from '../../query';
import { setupValueSuggestionProvider, ValueSuggestionsGetFn } from './value_suggestion_provider';
import { IUiSettingsClient, CoreSetup } from 'kibana/public';
-jest.mock('../../services', () => ({
- getQueryService: () => ({
- timefilter: {
- timefilter: {
- createFilter: () => {
- return {
- time: 'fake',
- };
- },
- getTime: () => {
- return {
- to: 'now',
- from: 'now-15m',
- };
- },
- },
- },
- }),
-}));
-
describe('FieldSuggestions', () => {
let getValueSuggestions: ValueSuggestionsGetFn;
let http: any;
@@ -50,7 +31,23 @@ describe('FieldSuggestions', () => {
const uiSettings = { get: (key: string) => shouldSuggestValues } as IUiSettingsClient;
http = { fetch: jest.fn() };
- getValueSuggestions = setupValueSuggestionProvider({ http, uiSettings } as CoreSetup);
+ getValueSuggestions = setupValueSuggestionProvider({ http, uiSettings } as CoreSetup, {
+ timefilter: ({
+ timefilter: {
+ createFilter: () => {
+ return {
+ time: 'fake',
+ };
+ },
+ getTime: () => {
+ return {
+ to: 'now',
+ from: 'now-15m',
+ };
+ },
+ },
+ } as unknown) as TimefilterSetup,
+ });
});
describe('with value suggestions disabled', () => {
diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts
index fe9f939a0261d..ee92fce02dda5 100644
--- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts
+++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts
@@ -21,7 +21,7 @@ import dateMath from '@elastic/datemath';
import { memoize } from 'lodash';
import { CoreSetup } from 'src/core/public';
import { IIndexPattern, IFieldType, UI_SETTINGS, buildQueryFromFilters } from '../../../common';
-import { getQueryService } from '../../services';
+import { TimefilterSetup } from '../../query';
function resolver(title: string, field: IFieldType, query: string, filters: any[]) {
// Only cache results for a minute
@@ -40,8 +40,10 @@ interface ValueSuggestionsGetFnArgs {
signal?: AbortSignal;
}
-const getAutocompleteTimefilter = (indexPattern: IIndexPattern) => {
- const { timefilter } = getQueryService().timefilter;
+const getAutocompleteTimefilter = (
+ { timefilter }: TimefilterSetup,
+ indexPattern: IIndexPattern
+) => {
const timeRange = timefilter.getTime();
// Use a rounded timerange so that memoizing works properly
@@ -54,7 +56,10 @@ const getAutocompleteTimefilter = (indexPattern: IIndexPattern) => {
export const getEmptyValueSuggestions = (() => Promise.resolve([])) as ValueSuggestionsGetFn;
-export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsGetFn => {
+export const setupValueSuggestionProvider = (
+ core: CoreSetup,
+ { timefilter }: { timefilter: TimefilterSetup }
+): ValueSuggestionsGetFn => {
const requestSuggestions = memoize(
(index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) =>
core.http.fetch(`/api/kibana/suggestions/values/${index}`, {
@@ -86,7 +91,9 @@ export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsG
return [];
}
- const timeFilter = useTimeRange ? getAutocompleteTimefilter(indexPattern) : undefined;
+ const timeFilter = useTimeRange
+ ? getAutocompleteTimefilter(timefilter, indexPattern)
+ : undefined;
const filterQuery = timeFilter ? buildQueryFromFilters([timeFilter], indexPattern).filter : [];
const filters = [...(boolFilter ? boolFilter : []), ...filterQuery];
return await requestSuggestions(title, field, query, filters, signal);
diff --git a/src/plugins/data/public/index_patterns/index_pattern.stub.ts b/src/plugins/data/public/index_patterns/index_pattern.stub.ts
index e5c6c008e3e28..804f0d7d89225 100644
--- a/src/plugins/data/public/index_patterns/index_pattern.stub.ts
+++ b/src/plugins/data/public/index_patterns/index_pattern.stub.ts
@@ -20,20 +20,9 @@
import sinon from 'sinon';
import { CoreSetup } from 'src/core/public';
-import { FieldFormat as FieldFormatImpl } from '../../common/field_formats';
import { IFieldType, FieldSpec } from '../../common/index_patterns';
-import { FieldFormatsStart } from '../field_formats';
import { IndexPattern, indexPatterns, KBN_FIELD_TYPES, fieldList } from '../';
import { getFieldFormatsRegistry } from '../test_utils';
-import { setFieldFormats } from '../services';
-
-setFieldFormats(({
- getDefaultInstance: () =>
- ({
- getConverterFor: () => (value: any) => value,
- convert: (value: any) => JSON.stringify(value),
- } as FieldFormatImpl),
-} as unknown) as FieldFormatsStart);
export function getStubIndexPattern(
pattern: string,
diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts
index afa8d935f367b..7e8283476ffc5 100644
--- a/src/plugins/data/public/plugin.ts
+++ b/src/plugins/data/public/plugin.ts
@@ -41,16 +41,14 @@ import {
UiSettingsPublicToCommon,
} from './index_patterns';
import {
- setFieldFormats,
setIndexPatterns,
setNotifications,
setOverlays,
- setQueryService,
setSearchService,
setUiSettings,
} from './services';
import { createSearchBar } from './ui/search_bar/create_search_bar';
-import { esaggs } from './search/expressions';
+import { getEsaggs } from './search/expressions';
import {
SELECT_RANGE_TRIGGER,
VALUE_CLICK_TRIGGER,
@@ -111,8 +109,22 @@ export class DataPublicPlugin
): DataPublicPluginSetup {
const startServices = createStartServicesGetter(core.getStartServices);
- expressions.registerFunction(esaggs);
expressions.registerFunction(indexPatternLoad);
+ expressions.registerFunction(
+ getEsaggs({
+ getStartDependencies: async () => {
+ const [, , self] = await core.getStartServices();
+ const { fieldFormats, indexPatterns, query, search } = self;
+ return {
+ addFilters: query.filterManager.addFilters.bind(query.filterManager),
+ aggs: search.aggs,
+ deserializeFieldFormat: fieldFormats.deserialize.bind(fieldFormats),
+ indexPatterns,
+ searchSource: search.searchSource,
+ };
+ },
+ })
+ );
this.usageCollection = usageCollection;
@@ -145,7 +157,7 @@ export class DataPublicPlugin
});
return {
- autocomplete: this.autocomplete.setup(core),
+ autocomplete: this.autocomplete.setup(core, { timefilter: queryService.timefilter }),
search: searchService,
fieldFormats: this.fieldFormatsService.setup(core),
query: queryService,
@@ -162,7 +174,6 @@ export class DataPublicPlugin
setUiSettings(uiSettings);
const fieldFormats = this.fieldFormatsService.start();
- setFieldFormats(fieldFormats);
const indexPatterns = new IndexPatternsService({
uiSettings: new UiSettingsPublicToCommon(uiSettings),
@@ -186,7 +197,6 @@ export class DataPublicPlugin
savedObjectsClient: savedObjects.client,
uiSettings,
});
- setQueryService(query);
const search = this.searchService.start(core, { fieldFormats, indexPatterns });
setSearchService(search);
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 0768658e40299..fc9b8d4839ea3 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -27,6 +27,7 @@ import { EuiButtonEmptyProps } from '@elastic/eui';
import { EuiComboBoxProps } from '@elastic/eui';
import { EuiConfirmModalProps } from '@elastic/eui';
import { EuiGlobalToastListToast } from '@elastic/eui';
+import { EventEmitter } from 'events';
import { ExclusiveUnion } from '@elastic/eui';
import { ExecutionContext } from 'src/plugins/expressions/common';
import { ExpressionAstFunction } from 'src/plugins/expressions/common';
@@ -66,13 +67,15 @@ import * as React_2 from 'react';
import { RecursiveReadonly } from '@kbn/utility-types';
import { Reporter } from '@kbn/analytics';
import { RequestAdapter } from 'src/plugins/inspector/common';
-import { RequestStatistics } from 'src/plugins/inspector/common';
+import { RequestStatistics as RequestStatistics_2 } from 'src/plugins/inspector/common';
import { Required } from '@kbn/utility-types';
import * as Rx from 'rxjs';
-import { SavedObject } from 'src/core/server';
-import { SavedObject as SavedObject_2 } from 'src/core/public';
+import { SavedObject } from 'kibana/server';
+import { SavedObject as SavedObject_2 } from 'src/core/server';
+import { SavedObject as SavedObject_3 } from 'src/core/public';
import { SavedObjectReference } from 'src/core/types';
import { SavedObjectsClientContract } from 'src/core/public';
+import { SavedObjectsFindResponse } from 'kibana/server';
import { Search } from '@elastic/elasticsearch/api/requestParams';
import { SearchResponse } from 'elasticsearch';
import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common';
@@ -975,7 +978,7 @@ export interface IFieldType {
// (undocumented)
count?: number;
// (undocumented)
- customName?: string;
+ customLabel?: string;
// (undocumented)
displayName?: string;
// (undocumented)
@@ -1149,7 +1152,7 @@ export class IndexPattern implements IIndexPattern {
// (undocumented)
getFieldAttrs: () => {
[x: string]: {
- customName: string;
+ customLabel: string;
};
};
// (undocumented)
@@ -1256,8 +1259,8 @@ export class IndexPatternField implements IFieldType {
get count(): number;
set count(count: number);
// (undocumented)
- get customName(): string | undefined;
- set customName(label: string | undefined);
+ get customLabel(): string | undefined;
+ set customLabel(customLabel: string | undefined);
// (undocumented)
get displayName(): string;
// (undocumented)
@@ -1296,7 +1299,7 @@ export class IndexPatternField implements IFieldType {
aggregatable: boolean;
readFromDocValues: boolean;
subType: import("../types").IFieldSubType | undefined;
- customName: string | undefined;
+ customLabel: string | undefined;
};
// (undocumented)
toSpec({ getFormatterForField, }?: {
@@ -1388,7 +1391,7 @@ export class IndexPatternsService {
// Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts
//
// (undocumented)
- getCache: () => Promise[] | null | undefined>;
+ getCache: () => Promise[] | null | undefined>;
getDefault: () => Promise;
getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise;
// Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts
@@ -1400,7 +1403,7 @@ export class IndexPatternsService {
}>>;
getTitles: (refresh?: boolean) => Promise;
refreshFields: (indexPattern: IndexPattern) => Promise;
- savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec;
+ savedObjectToSpec: (savedObject: SavedObject_2) => IndexPatternSpec;
setDefault: (id: string, force?: boolean) => Promise;
updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number, ignoreErrors?: boolean): Promise;
}
@@ -1445,6 +1448,8 @@ export type ISearchGeneric = | undefined
// @public (undocumented)
export interface ISessionService {
clear: () => void;
+ delete: (sessionId: string) => Promise;
+ // Warning: (ae-forgotten-export) The symbol "SearchSessionFindOptions" needs to be exported by the entry point index.d.ts
+ find: (options: SearchSessionFindOptions) => Promise>;
+ get: (sessionId: string) => Promise>;
getSession$: () => Observable;
getSessionId: () => string | undefined;
- restore: (sessionId: string) => void;
+ isRestore: () => boolean;
+ isStored: () => boolean;
+ // Warning: (ae-forgotten-export) The symbol "BackgroundSessionSavedObjectAttributes" needs to be exported by the entry point index.d.ts
+ restore: (sessionId: string) => Promise>;
+ save: (name: string, url: string) => Promise>;
start: () => string;
+ update: (sessionId: string, attributes: Partial) => Promise;
}
// Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -2068,7 +2082,7 @@ export class SearchInterceptor {
// @internal
protected pendingCount$: BehaviorSubject;
// @internal (undocumented)
- protected runSearch(request: IKibanaSearchRequest, signal: AbortSignal, strategy?: string): Promise;
+ protected runSearch(request: IKibanaSearchRequest, options?: ISearchOptions): Promise;
search(request: IKibanaSearchRequest, options?: ISearchOptions): Observable;
// @internal (undocumented)
protected setupAbortSignal({ abortSignal, timeout, }: {
diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts
deleted file mode 100644
index 3932484801fa8..0000000000000
--- a/src/plugins/data/public/search/expressions/esaggs.ts
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { get, hasIn } from 'lodash';
-import { i18n } from '@kbn/i18n';
-import { Datatable, DatatableColumn } from 'src/plugins/expressions/public';
-import { PersistedState } from '../../../../../plugins/visualizations/public';
-import { Adapters } from '../../../../../plugins/inspector/public';
-
-import {
- calculateBounds,
- EsaggsExpressionFunctionDefinition,
- Filter,
- getTime,
- IIndexPattern,
- isRangeFilter,
- Query,
- TimeRange,
-} from '../../../common';
-import {
- getRequestInspectorStats,
- getResponseInspectorStats,
- IAggConfigs,
- ISearchSource,
- tabifyAggResponse,
-} from '../../../common/search';
-
-import { FilterManager } from '../../query';
-import {
- getFieldFormats,
- getIndexPatterns,
- getQueryService,
- getSearchService,
-} from '../../services';
-import { buildTabularInspectorData } from './build_tabular_inspector_data';
-
-export interface RequestHandlerParams {
- searchSource: ISearchSource;
- aggs: IAggConfigs;
- timeRange?: TimeRange;
- timeFields?: string[];
- indexPattern?: IIndexPattern;
- query?: Query;
- filters?: Filter[];
- filterManager: FilterManager;
- uiState?: PersistedState;
- partialRows?: boolean;
- inspectorAdapters: Adapters;
- metricsAtAllLevels?: boolean;
- visParams?: any;
- abortSignal?: AbortSignal;
- searchSessionId?: string;
-}
-
-const name = 'esaggs';
-
-const handleCourierRequest = async ({
- searchSource,
- aggs,
- timeRange,
- timeFields,
- indexPattern,
- query,
- filters,
- partialRows,
- metricsAtAllLevels,
- inspectorAdapters,
- filterManager,
- abortSignal,
- searchSessionId,
-}: RequestHandlerParams) => {
- // Create a new search source that inherits the original search source
- // but has the appropriate timeRange applied via a filter.
- // This is a temporary solution until we properly pass down all required
- // information for the request to the request handler (https://github.com/elastic/kibana/issues/16641).
- // Using callParentStartHandlers: true we make sure, that the parent searchSource
- // onSearchRequestStart will be called properly even though we use an inherited
- // search source.
- const timeFilterSearchSource = searchSource.createChild({ callParentStartHandlers: true });
- const requestSearchSource = timeFilterSearchSource.createChild({ callParentStartHandlers: true });
-
- aggs.setTimeRange(timeRange as TimeRange);
-
- // For now we need to mirror the history of the passed search source, since
- // the request inspector wouldn't work otherwise.
- Object.defineProperty(requestSearchSource, 'history', {
- get() {
- return searchSource.history;
- },
- set(history) {
- return (searchSource.history = history);
- },
- });
-
- requestSearchSource.setField('aggs', function () {
- return aggs.toDsl(metricsAtAllLevels);
- });
-
- requestSearchSource.onRequestStart((paramSearchSource, options) => {
- return aggs.onSearchRequestStart(paramSearchSource, options);
- });
-
- // If timeFields have been specified, use the specified ones, otherwise use primary time field of index
- // pattern if it's available.
- const defaultTimeField = indexPattern?.getTimeField?.();
- const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : [];
- const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields;
-
- // If a timeRange has been specified and we had at least one timeField available, create range
- // filters for that those time fields
- if (timeRange && allTimeFields.length > 0) {
- timeFilterSearchSource.setField('filter', () => {
- return allTimeFields
- .map((fieldName) => getTime(indexPattern, timeRange, { fieldName }))
- .filter(isRangeFilter);
- });
- }
-
- requestSearchSource.setField('filter', filters);
- requestSearchSource.setField('query', query);
-
- inspectorAdapters.requests.reset();
- const request = inspectorAdapters.requests.start(
- i18n.translate('data.functions.esaggs.inspector.dataRequest.title', {
- defaultMessage: 'Data',
- }),
- {
- description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', {
- defaultMessage:
- 'This request queries Elasticsearch to fetch the data for the visualization.',
- }),
- searchSessionId,
- }
- );
- request.stats(getRequestInspectorStats(requestSearchSource));
-
- try {
- const response = await requestSearchSource.fetch({
- abortSignal,
- sessionId: searchSessionId,
- });
-
- request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response });
-
- (searchSource as any).rawResponse = response;
- } catch (e) {
- // Log any error during request to the inspector
- request.error({ json: e });
- throw e;
- } finally {
- // Add the request body no matter if things went fine or not
- requestSearchSource.getSearchRequestBody().then((req: unknown) => {
- request.json(req);
- });
- }
-
- // Note that rawResponse is not deeply cloned here, so downstream applications using courier
- // must take care not to mutate it, or it could have unintended side effects, e.g. displaying
- // response data incorrectly in the inspector.
- let resp = (searchSource as any).rawResponse;
- for (const agg of aggs.aggs) {
- if (hasIn(agg, 'type.postFlightRequest')) {
- resp = await agg.type.postFlightRequest(
- resp,
- aggs,
- agg,
- requestSearchSource,
- inspectorAdapters.requests,
- abortSignal
- );
- }
- }
-
- (searchSource as any).finalResponse = resp;
-
- const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null;
- const tabifyParams = {
- metricsAtAllLevels,
- partialRows,
- timeRange: parsedTimeRange
- ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields }
- : undefined,
- };
-
- const response = tabifyAggResponse(aggs, (searchSource as any).finalResponse, tabifyParams);
-
- (searchSource as any).tabifiedResponse = response;
-
- inspectorAdapters.data.setTabularLoader(
- () =>
- buildTabularInspectorData((searchSource as any).tabifiedResponse, {
- queryFilter: filterManager,
- deserializeFieldFormat: getFieldFormats().deserialize,
- }),
- { returnsFormattedValues: true }
- );
-
- return response;
-};
-
-export const esaggs = (): EsaggsExpressionFunctionDefinition => ({
- name,
- type: 'datatable',
- inputTypes: ['kibana_context', 'null'],
- help: i18n.translate('data.functions.esaggs.help', {
- defaultMessage: 'Run AggConfig aggregation',
- }),
- args: {
- index: {
- types: ['string'],
- help: '',
- },
- metricsAtAllLevels: {
- types: ['boolean'],
- default: false,
- help: '',
- },
- partialRows: {
- types: ['boolean'],
- default: false,
- help: '',
- },
- includeFormatHints: {
- types: ['boolean'],
- default: false,
- help: '',
- },
- aggConfigs: {
- types: ['string'],
- default: '""',
- help: '',
- },
- timeFields: {
- types: ['string'],
- help: '',
- multi: true,
- },
- },
- async fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId }) {
- const indexPatterns = getIndexPatterns();
- const { filterManager } = getQueryService();
- const searchService = getSearchService();
-
- const aggConfigsState = JSON.parse(args.aggConfigs);
- const indexPattern = await indexPatterns.get(args.index);
- const aggs = searchService.aggs.createAggConfigs(indexPattern, aggConfigsState);
-
- // we should move searchSource creation inside courier request handler
- const searchSource = await searchService.searchSource.create();
-
- searchSource.setField('index', indexPattern);
- searchSource.setField('size', 0);
-
- const resolvedTimeRange = input?.timeRange && calculateBounds(input.timeRange);
-
- const response = await handleCourierRequest({
- searchSource,
- aggs,
- indexPattern,
- timeRange: get(input, 'timeRange', undefined),
- query: get(input, 'query', undefined) as any,
- filters: get(input, 'filters', undefined),
- timeFields: args.timeFields,
- metricsAtAllLevels: args.metricsAtAllLevels,
- partialRows: args.partialRows,
- inspectorAdapters: inspectorAdapters as Adapters,
- filterManager,
- abortSignal: (abortSignal as unknown) as AbortSignal,
- searchSessionId: getSearchSessionId(),
- });
-
- const table: Datatable = {
- type: 'datatable',
- rows: response.rows,
- columns: response.columns.map((column) => {
- const cleanedColumn: DatatableColumn = {
- id: column.id,
- name: column.name,
- meta: {
- type: column.aggConfig.params.field?.type || 'number',
- field: column.aggConfig.params.field?.name,
- index: indexPattern.title,
- params: column.aggConfig.toSerializedFieldFormat(),
- source: 'esaggs',
- sourceParams: {
- indexPatternId: indexPattern.id,
- appliedTimeRange:
- column.aggConfig.params.field?.name &&
- input?.timeRange &&
- args.timeFields &&
- args.timeFields.includes(column.aggConfig.params.field?.name)
- ? {
- from: resolvedTimeRange?.min?.toISOString(),
- to: resolvedTimeRange?.max?.toISOString(),
- }
- : undefined,
- ...column.aggConfig.serialize(),
- },
- },
- };
- return cleanedColumn;
- }),
- };
-
- return table;
- },
-});
diff --git a/src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts b/src/plugins/data/public/search/expressions/esaggs/build_tabular_inspector_data.ts
similarity index 78%
rename from src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts
rename to src/plugins/data/public/search/expressions/esaggs/build_tabular_inspector_data.ts
index 7eff6f25fd828..79dedf4131764 100644
--- a/src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts
+++ b/src/plugins/data/public/search/expressions/esaggs/build_tabular_inspector_data.ts
@@ -18,35 +18,41 @@
*/
import { set } from '@elastic/safer-lodash-set';
-import { FormattedData } from '../../../../../plugins/inspector/public';
-import { TabbedTable } from '../../../common';
-import { FormatFactory } from '../../../common/field_formats/utils';
-import { createFilter } from './create_filter';
+import {
+ FormattedData,
+ TabularData,
+ TabularDataValue,
+} from '../../../../../../plugins/inspector/common';
+import { Filter, TabbedTable } from '../../../../common';
+import { FormatFactory } from '../../../../common/field_formats/utils';
+import { createFilter } from '../create_filter';
/**
- * @deprecated
+ * Type borrowed from the client-side FilterManager['addFilters'].
*
- * Do not use this function.
- *
- * @todo This function is used only by Courier. Courier will
- * soon be removed, and this function will be deleted, too. If Courier is not removed,
- * move this function inside Courier.
- *
- * ---
+ * We need to use a custom type to make this isomorphic since FilterManager
+ * doesn't exist on the server.
*
+ * @internal
+ */
+export type AddFilters = (filters: Filter[] | Filter, pinFilterStatus?: boolean) => void;
+
+/**
* This function builds tabular data from the response and attaches it to the
* inspector. It will only be called when the data view in the inspector is opened.
+ *
+ * @internal
*/
export async function buildTabularInspectorData(
table: TabbedTable,
{
- queryFilter,
+ addFilters,
deserializeFieldFormat,
}: {
- queryFilter: { addFilters: (filter: any) => void };
+ addFilters?: AddFilters;
deserializeFieldFormat: FormatFactory;
}
-) {
+): Promise {
const aggConfigs = table.columns.map((column) => column.aggConfig);
const rows = table.rows.map((row) => {
return table.columns.reduce>((prev, cur, colIndex) => {
@@ -74,20 +80,22 @@ export async function buildTabularInspectorData(
name: col.name,
field: `col-${colIndex}-${col.aggConfig.id}`,
filter:
+ addFilters &&
isCellContentFilterable &&
- ((value: { raw: unknown }) => {
+ ((value: TabularDataValue) => {
const rowIndex = rows.findIndex(
(row) => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw
);
const filter = createFilter(aggConfigs, table, colIndex, rowIndex, value.raw);
if (filter) {
- queryFilter.addFilters(filter);
+ addFilters(filter);
}
}),
filterOut:
+ addFilters &&
isCellContentFilterable &&
- ((value: { raw: unknown }) => {
+ ((value: TabularDataValue) => {
const rowIndex = rows.findIndex(
(row) => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw
);
@@ -101,7 +109,7 @@ export async function buildTabularInspectorData(
} else {
set(filter, 'meta.negate', notOther && notMissing);
}
- queryFilter.addFilters(filter);
+ addFilters(filter);
}
}),
};
diff --git a/src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts b/src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts
new file mode 100644
index 0000000000000..ce3bd9bdaee76
--- /dev/null
+++ b/src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts
@@ -0,0 +1,155 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { get } from 'lodash';
+import { i18n } from '@kbn/i18n';
+
+import { Datatable, DatatableColumn } from 'src/plugins/expressions/common';
+import { Adapters } from 'src/plugins/inspector/common';
+
+import { calculateBounds, EsaggsExpressionFunctionDefinition } from '../../../../common';
+import { FormatFactory } from '../../../../common/field_formats/utils';
+import { IndexPatternsContract } from '../../../../common/index_patterns/index_patterns';
+import { ISearchStartSearchSource, AggsStart } from '../../../../common/search';
+
+import { AddFilters } from './build_tabular_inspector_data';
+import { handleRequest } from './request_handler';
+
+const name = 'esaggs';
+
+interface StartDependencies {
+ addFilters: AddFilters;
+ aggs: AggsStart;
+ deserializeFieldFormat: FormatFactory;
+ indexPatterns: IndexPatternsContract;
+ searchSource: ISearchStartSearchSource;
+}
+
+export function getEsaggs({
+ getStartDependencies,
+}: {
+ getStartDependencies: () => Promise;
+}) {
+ return (): EsaggsExpressionFunctionDefinition => ({
+ name,
+ type: 'datatable',
+ inputTypes: ['kibana_context', 'null'],
+ help: i18n.translate('data.functions.esaggs.help', {
+ defaultMessage: 'Run AggConfig aggregation',
+ }),
+ args: {
+ index: {
+ types: ['string'],
+ help: '',
+ },
+ metricsAtAllLevels: {
+ types: ['boolean'],
+ default: false,
+ help: '',
+ },
+ partialRows: {
+ types: ['boolean'],
+ default: false,
+ help: '',
+ },
+ includeFormatHints: {
+ types: ['boolean'],
+ default: false,
+ help: '',
+ },
+ aggConfigs: {
+ types: ['string'],
+ default: '""',
+ help: '',
+ },
+ timeFields: {
+ types: ['string'],
+ help: '',
+ multi: true,
+ },
+ },
+ async fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId }) {
+ const {
+ addFilters,
+ aggs,
+ deserializeFieldFormat,
+ indexPatterns,
+ searchSource,
+ } = await getStartDependencies();
+
+ const aggConfigsState = JSON.parse(args.aggConfigs);
+ const indexPattern = await indexPatterns.get(args.index);
+ const aggConfigs = aggs.createAggConfigs(indexPattern, aggConfigsState);
+
+ const resolvedTimeRange = input?.timeRange && calculateBounds(input.timeRange);
+
+ const response = await handleRequest({
+ abortSignal: (abortSignal as unknown) as AbortSignal,
+ addFilters,
+ aggs: aggConfigs,
+ deserializeFieldFormat,
+ filters: get(input, 'filters', undefined),
+ indexPattern,
+ inspectorAdapters: inspectorAdapters as Adapters,
+ metricsAtAllLevels: args.metricsAtAllLevels,
+ partialRows: args.partialRows,
+ query: get(input, 'query', undefined) as any,
+ searchSessionId: getSearchSessionId(),
+ searchSourceService: searchSource,
+ timeFields: args.timeFields,
+ timeRange: get(input, 'timeRange', undefined),
+ });
+
+ const table: Datatable = {
+ type: 'datatable',
+ rows: response.rows,
+ columns: response.columns.map((column) => {
+ const cleanedColumn: DatatableColumn = {
+ id: column.id,
+ name: column.name,
+ meta: {
+ type: column.aggConfig.params.field?.type || 'number',
+ field: column.aggConfig.params.field?.name,
+ index: indexPattern.title,
+ params: column.aggConfig.toSerializedFieldFormat(),
+ source: name,
+ sourceParams: {
+ indexPatternId: indexPattern.id,
+ appliedTimeRange:
+ column.aggConfig.params.field?.name &&
+ input?.timeRange &&
+ args.timeFields &&
+ args.timeFields.includes(column.aggConfig.params.field?.name)
+ ? {
+ from: resolvedTimeRange?.min?.toISOString(),
+ to: resolvedTimeRange?.max?.toISOString(),
+ }
+ : undefined,
+ ...column.aggConfig.serialize(),
+ },
+ },
+ };
+ return cleanedColumn;
+ }),
+ };
+
+ return table;
+ },
+ });
+}
diff --git a/src/plugins/data/public/search/expressions/esaggs/index.ts b/src/plugins/data/public/search/expressions/esaggs/index.ts
new file mode 100644
index 0000000000000..cbd3fb9cc5e91
--- /dev/null
+++ b/src/plugins/data/public/search/expressions/esaggs/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export * from './esaggs_fn';
diff --git a/src/plugins/data/public/search/expressions/esaggs/request_handler.ts b/src/plugins/data/public/search/expressions/esaggs/request_handler.ts
new file mode 100644
index 0000000000000..93b5705b821c0
--- /dev/null
+++ b/src/plugins/data/public/search/expressions/esaggs/request_handler.ts
@@ -0,0 +1,213 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { Adapters } from 'src/plugins/inspector/common';
+
+import {
+ calculateBounds,
+ Filter,
+ getTime,
+ IndexPattern,
+ isRangeFilter,
+ Query,
+ TimeRange,
+} from '../../../../common';
+import {
+ getRequestInspectorStats,
+ getResponseInspectorStats,
+ IAggConfigs,
+ ISearchStartSearchSource,
+ tabifyAggResponse,
+} from '../../../../common/search';
+import { FormatFactory } from '../../../../common/field_formats/utils';
+
+import { AddFilters, buildTabularInspectorData } from './build_tabular_inspector_data';
+
+interface RequestHandlerParams {
+ abortSignal?: AbortSignal;
+ addFilters?: AddFilters;
+ aggs: IAggConfigs;
+ deserializeFieldFormat: FormatFactory;
+ filters?: Filter[];
+ indexPattern?: IndexPattern;
+ inspectorAdapters: Adapters;
+ metricsAtAllLevels?: boolean;
+ partialRows?: boolean;
+ query?: Query;
+ searchSessionId?: string;
+ searchSourceService: ISearchStartSearchSource;
+ timeFields?: string[];
+ timeRange?: TimeRange;
+}
+
+export const handleRequest = async ({
+ abortSignal,
+ addFilters,
+ aggs,
+ deserializeFieldFormat,
+ filters,
+ indexPattern,
+ inspectorAdapters,
+ metricsAtAllLevels,
+ partialRows,
+ query,
+ searchSessionId,
+ searchSourceService,
+ timeFields,
+ timeRange,
+}: RequestHandlerParams) => {
+ const searchSource = await searchSourceService.create();
+
+ searchSource.setField('index', indexPattern);
+ searchSource.setField('size', 0);
+
+ // Create a new search source that inherits the original search source
+ // but has the appropriate timeRange applied via a filter.
+ // This is a temporary solution until we properly pass down all required
+ // information for the request to the request handler (https://github.com/elastic/kibana/issues/16641).
+ // Using callParentStartHandlers: true we make sure, that the parent searchSource
+ // onSearchRequestStart will be called properly even though we use an inherited
+ // search source.
+ const timeFilterSearchSource = searchSource.createChild({ callParentStartHandlers: true });
+ const requestSearchSource = timeFilterSearchSource.createChild({ callParentStartHandlers: true });
+
+ aggs.setTimeRange(timeRange as TimeRange);
+
+ // For now we need to mirror the history of the passed search source, since
+ // the request inspector wouldn't work otherwise.
+ Object.defineProperty(requestSearchSource, 'history', {
+ get() {
+ return searchSource.history;
+ },
+ set(history) {
+ return (searchSource.history = history);
+ },
+ });
+
+ requestSearchSource.setField('aggs', function () {
+ return aggs.toDsl(metricsAtAllLevels);
+ });
+
+ requestSearchSource.onRequestStart((paramSearchSource, options) => {
+ return aggs.onSearchRequestStart(paramSearchSource, options);
+ });
+
+ // If timeFields have been specified, use the specified ones, otherwise use primary time field of index
+ // pattern if it's available.
+ const defaultTimeField = indexPattern?.getTimeField?.();
+ const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : [];
+ const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields;
+
+ // If a timeRange has been specified and we had at least one timeField available, create range
+ // filters for that those time fields
+ if (timeRange && allTimeFields.length > 0) {
+ timeFilterSearchSource.setField('filter', () => {
+ return allTimeFields
+ .map((fieldName) => getTime(indexPattern, timeRange, { fieldName }))
+ .filter(isRangeFilter);
+ });
+ }
+
+ requestSearchSource.setField('filter', filters);
+ requestSearchSource.setField('query', query);
+
+ let request;
+ if (inspectorAdapters.requests) {
+ inspectorAdapters.requests.reset();
+ request = inspectorAdapters.requests.start(
+ i18n.translate('data.functions.esaggs.inspector.dataRequest.title', {
+ defaultMessage: 'Data',
+ }),
+ {
+ description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', {
+ defaultMessage:
+ 'This request queries Elasticsearch to fetch the data for the visualization.',
+ }),
+ searchSessionId,
+ }
+ );
+ request.stats(getRequestInspectorStats(requestSearchSource));
+ }
+
+ try {
+ const response = await requestSearchSource.fetch({
+ abortSignal,
+ sessionId: searchSessionId,
+ });
+
+ if (request) {
+ request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response });
+ }
+
+ (searchSource as any).rawResponse = response;
+ } catch (e) {
+ // Log any error during request to the inspector
+ if (request) {
+ request.error({ json: e });
+ }
+ throw e;
+ } finally {
+ // Add the request body no matter if things went fine or not
+ if (request) {
+ request.json(await requestSearchSource.getSearchRequestBody());
+ }
+ }
+
+ // Note that rawResponse is not deeply cloned here, so downstream applications using courier
+ // must take care not to mutate it, or it could have unintended side effects, e.g. displaying
+ // response data incorrectly in the inspector.
+ let response = (searchSource as any).rawResponse;
+ for (const agg of aggs.aggs) {
+ if (typeof agg.type.postFlightRequest === 'function') {
+ response = await agg.type.postFlightRequest(
+ response,
+ aggs,
+ agg,
+ requestSearchSource,
+ inspectorAdapters.requests,
+ abortSignal
+ );
+ }
+ }
+
+ const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null;
+ const tabifyParams = {
+ metricsAtAllLevels,
+ partialRows,
+ timeRange: parsedTimeRange
+ ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields }
+ : undefined,
+ };
+
+ const tabifiedResponse = tabifyAggResponse(aggs, response, tabifyParams);
+
+ if (inspectorAdapters.data) {
+ inspectorAdapters.data.setTabularLoader(
+ () =>
+ buildTabularInspectorData(tabifiedResponse, {
+ addFilters,
+ deserializeFieldFormat,
+ }),
+ { returnsFormattedValues: true }
+ );
+ }
+
+ return tabifiedResponse;
+};
diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts
index 78e65802bcf99..3fadb723b27cd 100644
--- a/src/plugins/data/public/search/search_interceptor.ts
+++ b/src/plugins/data/public/search/search_interceptor.ts
@@ -126,18 +126,25 @@ export class SearchInterceptor {
*/
protected runSearch(
request: IKibanaSearchRequest,
- signal: AbortSignal,
- strategy?: string
+ options?: ISearchOptions
): Promise {
const { id, ...searchRequest } = request;
- const path = trimEnd(`/internal/search/${strategy || ES_SEARCH_STRATEGY}/${id || ''}`, '/');
- const body = JSON.stringify(searchRequest);
+ const path = trimEnd(
+ `/internal/search/${options?.strategy ?? ES_SEARCH_STRATEGY}/${id ?? ''}`,
+ '/'
+ );
+ const body = JSON.stringify({
+ sessionId: options?.sessionId,
+ isStored: options?.isStored,
+ isRestore: options?.isRestore,
+ ...searchRequest,
+ });
return this.deps.http.fetch({
method: 'POST',
path,
body,
- signal,
+ signal: options?.abortSignal,
});
}
@@ -235,7 +242,7 @@ export class SearchInterceptor {
abortSignal: options?.abortSignal,
});
this.pendingCount$.next(this.pendingCount$.getValue() + 1);
- return from(this.runSearch(request, combinedSignal, options?.strategy)).pipe(
+ return from(this.runSearch(request, { ...options, abortSignal: combinedSignal })).pipe(
catchError((e: Error) => {
return throwError(this.handleSearchError(e, request, timeoutSignal, options));
}),
diff --git a/src/plugins/data/public/search/session_service.ts b/src/plugins/data/public/search/session_service.ts
index a172738812937..0141cff258a9f 100644
--- a/src/plugins/data/public/search/session_service.ts
+++ b/src/plugins/data/public/search/session_service.ts
@@ -19,9 +19,13 @@
import uuid from 'uuid';
import { BehaviorSubject, Subscription } from 'rxjs';
-import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public';
+import { HttpStart, PluginInitializerContext, StartServicesAccessor } from 'kibana/public';
import { ConfigSchema } from '../../config';
-import { ISessionService } from '../../common/search';
+import {
+ ISessionService,
+ BackgroundSessionSavedObjectAttributes,
+ SearchSessionFindOptions,
+} from '../../common';
export class SessionService implements ISessionService {
private session$ = new BehaviorSubject(undefined);
@@ -30,6 +34,18 @@ export class SessionService implements ISessionService {
}
private appChangeSubscription$?: Subscription;
private curApp?: string;
+ private http!: HttpStart;
+
+ /**
+ * Has the session already been stored (i.e. "sent to background")?
+ */
+ private _isStored: boolean = false;
+
+ /**
+ * Is this session a restored session (have these requests already been made, and we're just
+ * looking to re-use the previous search IDs)?
+ */
+ private _isRestore: boolean = false;
constructor(
initializerContext: PluginInitializerContext,
@@ -39,6 +55,8 @@ export class SessionService implements ISessionService {
Make sure that apps don't leave sessions open.
*/
getStartServices().then(([coreStart]) => {
+ this.http = coreStart.http;
+
this.appChangeSubscription$ = coreStart.application.currentAppId$.subscribe((appName) => {
if (this.sessionId) {
const message = `Application '${this.curApp}' had an open session while navigating`;
@@ -69,16 +87,63 @@ export class SessionService implements ISessionService {
return this.session$.asObservable();
}
+ public isStored() {
+ return this._isStored;
+ }
+
+ public isRestore() {
+ return this._isRestore;
+ }
+
public start() {
+ this._isStored = false;
+ this._isRestore = false;
this.session$.next(uuid.v4());
return this.sessionId!;
}
public restore(sessionId: string) {
+ this._isStored = true;
+ this._isRestore = true;
this.session$.next(sessionId);
+ return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`);
}
public clear() {
+ this._isStored = false;
+ this._isRestore = false;
this.session$.next(undefined);
}
+
+ public async save(name: string, url: string) {
+ const response = await this.http.post(`/internal/session`, {
+ body: JSON.stringify({
+ name,
+ url,
+ sessionId: this.sessionId,
+ }),
+ });
+ this._isStored = true;
+ return response;
+ }
+
+ public get(sessionId: string) {
+ return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`);
+ }
+
+ public find(options: SearchSessionFindOptions) {
+ return this.http.post(`/internal/session`, {
+ body: JSON.stringify(options),
+ });
+ }
+
+ public update(sessionId: string, attributes: Partial) {
+ return this.http.put(`/internal/session/${encodeURIComponent(sessionId)}`, {
+ body: JSON.stringify(attributes),
+ });
+ }
+
+ public delete(sessionId: string) {
+ return this.http.delete(`/internal/session/${encodeURIComponent(sessionId)}`);
+ }
}
diff --git a/src/plugins/data/public/services.ts b/src/plugins/data/public/services.ts
index 032bce6d8d2aa..28fb4ff8b53ae 100644
--- a/src/plugins/data/public/services.ts
+++ b/src/plugins/data/public/services.ts
@@ -18,7 +18,6 @@
*/
import { NotificationsStart, CoreStart } from 'src/core/public';
-import { FieldFormatsStart } from './field_formats';
import { createGetterSetter } from '../../kibana_utils/public';
import { IndexPatternsContract } from './index_patterns';
import { DataPublicPluginStart } from './types';
@@ -31,20 +30,12 @@ export const [getUiSettings, setUiSettings] = createGetterSetter(
- 'FieldFormats'
-);
-
export const [getOverlays, setOverlays] = createGetterSetter('Overlays');
export const [getIndexPatterns, setIndexPatterns] = createGetterSetter(
'IndexPatterns'
);
-export const [getQueryService, setQueryService] = createGetterSetter<
- DataPublicPluginStart['query']
->('Query');
-
export const [getSearchService, setSearchService] = createGetterSetter<
DataPublicPluginStart['search']
>('Search');
diff --git a/src/plugins/data/server/saved_objects/background_session.ts b/src/plugins/data/server/saved_objects/background_session.ts
new file mode 100644
index 0000000000000..74b03c4d867e4
--- /dev/null
+++ b/src/plugins/data/server/saved_objects/background_session.ts
@@ -0,0 +1,56 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { SavedObjectsType } from 'kibana/server';
+
+export const BACKGROUND_SESSION_TYPE = 'background-session';
+
+export const backgroundSessionMapping: SavedObjectsType = {
+ name: BACKGROUND_SESSION_TYPE,
+ namespaceType: 'single',
+ hidden: true,
+ mappings: {
+ properties: {
+ name: {
+ type: 'keyword',
+ },
+ created: {
+ type: 'date',
+ },
+ expires: {
+ type: 'date',
+ },
+ status: {
+ type: 'keyword',
+ },
+ initialState: {
+ type: 'object',
+ enabled: false,
+ },
+ restoreState: {
+ type: 'object',
+ enabled: false,
+ },
+ idMapping: {
+ type: 'object',
+ enabled: false,
+ },
+ },
+ },
+};
diff --git a/src/plugins/data/server/saved_objects/index.ts b/src/plugins/data/server/saved_objects/index.ts
index 077f9380823d0..7cd4d319e6417 100644
--- a/src/plugins/data/server/saved_objects/index.ts
+++ b/src/plugins/data/server/saved_objects/index.ts
@@ -20,3 +20,4 @@ export { querySavedObjectType } from './query';
export { indexPatternSavedObjectType } from './index_patterns';
export { kqlTelemetry } from './kql_telemetry';
export { searchTelemetry } from './search_telemetry';
+export { BACKGROUND_SESSION_TYPE, backgroundSessionMapping } from './background_session';
diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts
index 4914726c85ef8..290e94ee7cf99 100644
--- a/src/plugins/data/server/search/mocks.ts
+++ b/src/plugins/data/server/search/mocks.ts
@@ -17,6 +17,8 @@
* under the License.
*/
+import type { RequestHandlerContext } from 'src/core/server';
+import { coreMock } from '../../../../core/server/mocks';
import { ISearchSetup, ISearchStart } from './types';
import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks';
import { searchSourceMock } from './search_source/mocks';
@@ -40,3 +42,22 @@ export function createSearchStartMock(): jest.Mocked {
searchSource: searchSourceMock.createStartContract(),
};
}
+
+export function createSearchRequestHandlerContext(): jest.Mocked {
+ return {
+ core: coreMock.createRequestHandlerContext(),
+ search: {
+ search: jest.fn(),
+ cancel: jest.fn(),
+ session: {
+ save: jest.fn(),
+ get: jest.fn(),
+ find: jest.fn(),
+ update: jest.fn(),
+ delete: jest.fn(),
+ trackId: jest.fn(),
+ getId: jest.fn(),
+ },
+ },
+ };
+}
diff --git a/src/plugins/data/server/search/routes/search.ts b/src/plugins/data/server/search/routes/search.ts
index a4161fe47b388..ed519164c8e43 100644
--- a/src/plugins/data/server/search/routes/search.ts
+++ b/src/plugins/data/server/search/routes/search.ts
@@ -35,11 +35,18 @@ export function registerSearchRoute(router: IRouter): void {
query: schema.object({}, { unknowns: 'allow' }),
- body: schema.object({}, { unknowns: 'allow' }),
+ body: schema.object(
+ {
+ sessionId: schema.maybe(schema.string()),
+ isStored: schema.maybe(schema.boolean()),
+ isRestore: schema.maybe(schema.boolean()),
+ },
+ { unknowns: 'allow' }
+ ),
},
},
async (context, request, res) => {
- const searchRequest = request.body;
+ const { sessionId, isStored, isRestore, ...searchRequest } = request.body;
const { strategy, id } = request.params;
const abortSignal = getRequestAbortedSignal(request.events.aborted$);
@@ -50,6 +57,9 @@ export function registerSearchRoute(router: IRouter): void {
{
abortSignal,
strategy,
+ sessionId,
+ isStored,
+ isRestore,
}
)
.pipe(first())
diff --git a/src/plugins/data/server/search/routes/session.test.ts b/src/plugins/data/server/search/routes/session.test.ts
new file mode 100644
index 0000000000000..f697f6d5a5c2b
--- /dev/null
+++ b/src/plugins/data/server/search/routes/session.test.ts
@@ -0,0 +1,119 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { MockedKeys } from '@kbn/utility-types/jest';
+import type { CoreSetup, RequestHandlerContext } from 'kibana/server';
+import type { DataPluginStart } from '../../plugin';
+import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks';
+import { createSearchRequestHandlerContext } from '../mocks';
+import { registerSessionRoutes } from './session';
+
+describe('registerSessionRoutes', () => {
+ let mockCoreSetup: MockedKeys>;
+ let mockContext: jest.Mocked;
+
+ beforeEach(() => {
+ mockCoreSetup = coreMock.createSetup();
+ mockContext = createSearchRequestHandlerContext();
+ registerSessionRoutes(mockCoreSetup.http.createRouter());
+ });
+
+ it('save calls session.save with sessionId and attributes', async () => {
+ const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
+ const name = 'my saved background search session';
+ const body = { sessionId, name };
+
+ const mockRequest = httpServerMock.createKibanaRequest({ body });
+ const mockResponse = httpServerMock.createResponseFactory();
+
+ const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
+ const [[, saveHandler]] = mockRouter.post.mock.calls;
+
+ saveHandler(mockContext, mockRequest, mockResponse);
+
+ expect(mockContext.search!.session.save).toHaveBeenCalledWith(sessionId, { name });
+ });
+
+ it('get calls session.get with sessionId', async () => {
+ const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
+ const params = { id };
+
+ const mockRequest = httpServerMock.createKibanaRequest({ params });
+ const mockResponse = httpServerMock.createResponseFactory();
+
+ const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
+ const [[, getHandler]] = mockRouter.get.mock.calls;
+
+ getHandler(mockContext, mockRequest, mockResponse);
+
+ expect(mockContext.search!.session.get).toHaveBeenCalledWith(id);
+ });
+
+ it('find calls session.find with options', async () => {
+ const page = 1;
+ const perPage = 5;
+ const sortField = 'my_field';
+ const sortOrder = 'desc';
+ const filter = 'foo: bar';
+ const body = { page, perPage, sortField, sortOrder, filter };
+
+ const mockRequest = httpServerMock.createKibanaRequest({ body });
+ const mockResponse = httpServerMock.createResponseFactory();
+
+ const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
+ const [, [, findHandler]] = mockRouter.post.mock.calls;
+
+ findHandler(mockContext, mockRequest, mockResponse);
+
+ expect(mockContext.search!.session.find).toHaveBeenCalledWith(body);
+ });
+
+ it('update calls session.update with id and attributes', async () => {
+ const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
+ const name = 'my saved background search session';
+ const expires = new Date().toISOString();
+ const params = { id };
+ const body = { name, expires };
+
+ const mockRequest = httpServerMock.createKibanaRequest({ params, body });
+ const mockResponse = httpServerMock.createResponseFactory();
+
+ const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
+ const [[, updateHandler]] = mockRouter.put.mock.calls;
+
+ updateHandler(mockContext, mockRequest, mockResponse);
+
+ expect(mockContext.search!.session.update).toHaveBeenCalledWith(id, body);
+ });
+
+ it('delete calls session.delete with id', async () => {
+ const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
+ const params = { id };
+
+ const mockRequest = httpServerMock.createKibanaRequest({ params });
+ const mockResponse = httpServerMock.createResponseFactory();
+
+ const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
+ const [[, deleteHandler]] = mockRouter.delete.mock.calls;
+
+ deleteHandler(mockContext, mockRequest, mockResponse);
+
+ expect(mockContext.search!.session.delete).toHaveBeenCalledWith(id);
+ });
+});
diff --git a/src/plugins/data/server/search/routes/session.ts b/src/plugins/data/server/search/routes/session.ts
new file mode 100644
index 0000000000000..93f07ecfb92ff
--- /dev/null
+++ b/src/plugins/data/server/search/routes/session.ts
@@ -0,0 +1,201 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { schema } from '@kbn/config-schema';
+import { IRouter } from 'src/core/server';
+
+export function registerSessionRoutes(router: IRouter): void {
+ router.post(
+ {
+ path: '/internal/session',
+ validate: {
+ body: schema.object({
+ sessionId: schema.string(),
+ name: schema.string(),
+ expires: schema.maybe(schema.string()),
+ initialState: schema.maybe(schema.object({}, { unknowns: 'allow' })),
+ restoreState: schema.maybe(schema.object({}, { unknowns: 'allow' })),
+ }),
+ },
+ },
+ async (context, request, res) => {
+ const { sessionId, name, expires, initialState, restoreState } = request.body;
+
+ try {
+ const response = await context.search!.session.save(sessionId, {
+ name,
+ expires,
+ initialState,
+ restoreState,
+ });
+
+ return res.ok({
+ body: response,
+ });
+ } catch (err) {
+ return res.customError({
+ statusCode: err.statusCode || 500,
+ body: {
+ message: err.message,
+ attributes: {
+ error: err.body?.error || err.message,
+ },
+ },
+ });
+ }
+ }
+ );
+
+ router.get(
+ {
+ path: '/internal/session/{id}',
+ validate: {
+ params: schema.object({
+ id: schema.string(),
+ }),
+ },
+ },
+ async (context, request, res) => {
+ const { id } = request.params;
+ try {
+ const response = await context.search!.session.get(id);
+
+ return res.ok({
+ body: response,
+ });
+ } catch (err) {
+ return res.customError({
+ statusCode: err.statusCode || 500,
+ body: {
+ message: err.message,
+ attributes: {
+ error: err.body?.error || err.message,
+ },
+ },
+ });
+ }
+ }
+ );
+
+ router.post(
+ {
+ path: '/internal/session/_find',
+ validate: {
+ body: schema.object({
+ page: schema.maybe(schema.number()),
+ perPage: schema.maybe(schema.number()),
+ sortField: schema.maybe(schema.string()),
+ sortOrder: schema.maybe(schema.string()),
+ filter: schema.maybe(schema.string()),
+ }),
+ },
+ },
+ async (context, request, res) => {
+ const { page, perPage, sortField, sortOrder, filter } = request.body;
+ try {
+ const response = await context.search!.session.find({
+ page,
+ perPage,
+ sortField,
+ sortOrder,
+ filter,
+ });
+
+ return res.ok({
+ body: response,
+ });
+ } catch (err) {
+ return res.customError({
+ statusCode: err.statusCode || 500,
+ body: {
+ message: err.message,
+ attributes: {
+ error: err.body?.error || err.message,
+ },
+ },
+ });
+ }
+ }
+ );
+
+ router.delete(
+ {
+ path: '/internal/session/{id}',
+ validate: {
+ params: schema.object({
+ id: schema.string(),
+ }),
+ },
+ },
+ async (context, request, res) => {
+ const { id } = request.params;
+ try {
+ await context.search!.session.delete(id);
+
+ return res.ok();
+ } catch (err) {
+ return res.customError({
+ statusCode: err.statusCode || 500,
+ body: {
+ message: err.message,
+ attributes: {
+ error: err.body?.error || err.message,
+ },
+ },
+ });
+ }
+ }
+ );
+
+ router.put(
+ {
+ path: '/internal/session/{id}',
+ validate: {
+ params: schema.object({
+ id: schema.string(),
+ }),
+ body: schema.object({
+ name: schema.maybe(schema.string()),
+ expires: schema.maybe(schema.string()),
+ }),
+ },
+ },
+ async (context, request, res) => {
+ const { id } = request.params;
+ const { name, expires } = request.body;
+ try {
+ const response = await context.search!.session.update(id, { name, expires });
+
+ return res.ok({
+ body: response,
+ });
+ } catch (err) {
+ return res.customError({
+ statusCode: err.statusCode || 500,
+ body: {
+ message: err.message,
+ attributes: {
+ error: err.body?.error || err.message,
+ },
+ },
+ });
+ }
+ }
+ );
+}
diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts
index d8aa588719e3e..b44980164d097 100644
--- a/src/plugins/data/server/search/search_service.ts
+++ b/src/plugins/data/server/search/search_service.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { BehaviorSubject, Observable } from 'rxjs';
+import { BehaviorSubject, from, Observable } from 'rxjs';
import { pick } from 'lodash';
import {
CoreSetup,
@@ -29,7 +29,7 @@ import {
SharedGlobalConfig,
StartServicesAccessor,
} from 'src/core/server';
-import { first } from 'rxjs/operators';
+import { first, switchMap } from 'rxjs/operators';
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import {
ISearchSetup,
@@ -49,7 +49,7 @@ import { DataPluginStart } from '../plugin';
import { UsageCollectionSetup } from '../../../usage_collection/server';
import { registerUsageCollector } from './collectors/register';
import { usageProvider } from './collectors/usage';
-import { searchTelemetry } from '../saved_objects';
+import { BACKGROUND_SESSION_TYPE, searchTelemetry } from '../saved_objects';
import {
IEsSearchRequest,
IEsSearchResponse,
@@ -70,10 +70,14 @@ import {
} from '../../common/search/aggs/buckets/shard_delay';
import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn';
import { ConfigSchema } from '../../config';
+import { BackgroundSessionService, ISearchSessionClient } from './session';
+import { registerSessionRoutes } from './routes/session';
+import { backgroundSessionMapping } from '../saved_objects';
+import { tapFirst } from '../../common/utils';
declare module 'src/core/server' {
interface RequestHandlerContext {
- search?: ISearchClient;
+ search?: ISearchClient & { session: ISearchSessionClient };
}
}
@@ -102,6 +106,7 @@ export class SearchService implements Plugin {
private readonly searchSourceService = new SearchSourceService();
private defaultSearchStrategyName: string = ES_SEARCH_STRATEGY;
private searchStrategies: StrategyMap = {};
+ private sessionService: BackgroundSessionService = new BackgroundSessionService();
constructor(
private initializerContext: PluginInitializerContext,
@@ -121,12 +126,17 @@ export class SearchService implements Plugin {
};
registerSearchRoute(router);
registerMsearchRoute(router, routeDependencies);
+ registerSessionRoutes(router);
core.http.registerRouteHandlerContext('search', async (context, request) => {
const [coreStart] = await core.getStartServices();
- return this.asScopedProvider(coreStart)(request);
+ const search = this.asScopedProvider(coreStart)(request);
+ const session = this.sessionService.asScopedProvider(coreStart)(request);
+ return { ...search, session };
});
+ core.savedObjects.registerType(backgroundSessionMapping);
+
this.registerSearchStrategy(
ES_SEARCH_STRATEGY,
esSearchStrategyProvider(
@@ -223,6 +233,7 @@ export class SearchService implements Plugin {
public stop() {
this.aggsService.stop();
+ this.sessionService.stop();
}
private registerSearchStrategy = <
@@ -248,7 +259,24 @@ export class SearchService implements Plugin {
options.strategy
);
- return strategy.search(searchRequest, options, deps);
+ // If this is a restored background search session, look up the ID using the provided sessionId
+ const getSearchRequest = async () =>
+ !options.isRestore || searchRequest.id
+ ? searchRequest
+ : {
+ ...searchRequest,
+ id: await this.sessionService.getId(searchRequest, options, deps),
+ };
+
+ return from(getSearchRequest()).pipe(
+ switchMap((request) => strategy.search(request, options, deps)),
+ tapFirst((response) => {
+ if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return;
+ this.sessionService.trackId(searchRequest, response.id, options, {
+ savedObjectsClient: deps.savedObjectsClient,
+ });
+ })
+ );
};
private cancel = (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => {
@@ -273,7 +301,9 @@ export class SearchService implements Plugin {
private asScopedProvider = ({ elasticsearch, savedObjects, uiSettings }: CoreStart) => {
return (request: KibanaRequest): ISearchClient => {
- const savedObjectsClient = savedObjects.getScopedClient(request);
+ const savedObjectsClient = savedObjects.getScopedClient(request, {
+ includedHiddenTypes: [BACKGROUND_SESSION_TYPE],
+ });
const deps = {
savedObjectsClient,
esClient: elasticsearch.client.asScoped(request),
diff --git a/src/plugins/data/server/search/session/index.ts b/src/plugins/data/server/search/session/index.ts
new file mode 100644
index 0000000000000..11b5b16a02b56
--- /dev/null
+++ b/src/plugins/data/server/search/session/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { BackgroundSessionService, ISearchSessionClient } from './session_service';
diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts
new file mode 100644
index 0000000000000..1ceebae967d4c
--- /dev/null
+++ b/src/plugins/data/server/search/session/session_service.test.ts
@@ -0,0 +1,233 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { SavedObject, SavedObjectsClientContract } from 'kibana/server';
+import { savedObjectsClientMock } from '../../../../../core/server/mocks';
+import { BackgroundSessionStatus } from '../../../common';
+import { BACKGROUND_SESSION_TYPE } from '../../saved_objects';
+import { BackgroundSessionService } from './session_service';
+import { createRequestHash } from './utils';
+
+describe('BackgroundSessionService', () => {
+ let savedObjectsClient: jest.Mocked;
+ let service: BackgroundSessionService;
+
+ const mockSavedObject: SavedObject = {
+ id: 'd7170a35-7e2c-48d6-8dec-9a056721b489',
+ type: BACKGROUND_SESSION_TYPE,
+ attributes: {
+ name: 'my_name',
+ idMapping: {},
+ },
+ references: [],
+ };
+
+ beforeEach(() => {
+ savedObjectsClient = savedObjectsClientMock.create();
+ service = new BackgroundSessionService();
+ });
+
+ it('save throws if `name` is not provided', () => {
+ const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
+
+ expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot(
+ `[Error: Name is required]`
+ );
+ });
+
+ it('get calls saved objects client', async () => {
+ savedObjectsClient.get.mockResolvedValue(mockSavedObject);
+
+ const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
+ const response = await service.get(sessionId, { savedObjectsClient });
+
+ expect(response).toBe(mockSavedObject);
+ expect(savedObjectsClient.get).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId);
+ });
+
+ it('find calls saved objects client', async () => {
+ const mockFindSavedObject = {
+ ...mockSavedObject,
+ score: 1,
+ };
+ const mockResponse = {
+ saved_objects: [mockFindSavedObject],
+ total: 1,
+ per_page: 1,
+ page: 0,
+ };
+ savedObjectsClient.find.mockResolvedValue(mockResponse);
+
+ const options = { page: 0, perPage: 5 };
+ const response = await service.find(options, { savedObjectsClient });
+
+ expect(response).toBe(mockResponse);
+ expect(savedObjectsClient.find).toHaveBeenCalledWith({
+ ...options,
+ type: BACKGROUND_SESSION_TYPE,
+ });
+ });
+
+ it('update calls saved objects client', async () => {
+ const mockUpdateSavedObject = {
+ ...mockSavedObject,
+ attributes: {},
+ };
+ savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject);
+
+ const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
+ const attributes = { name: 'new_name' };
+ const response = await service.update(sessionId, attributes, { savedObjectsClient });
+
+ expect(response).toBe(mockUpdateSavedObject);
+ expect(savedObjectsClient.update).toHaveBeenCalledWith(
+ BACKGROUND_SESSION_TYPE,
+ sessionId,
+ attributes
+ );
+ });
+
+ it('delete calls saved objects client', async () => {
+ savedObjectsClient.delete.mockResolvedValue({});
+
+ const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
+ const response = await service.delete(sessionId, { savedObjectsClient });
+
+ expect(response).toEqual({});
+ expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId);
+ });
+
+ describe('trackId', () => {
+ it('stores hash in memory when `isStored` is `false` for when `save` is called', async () => {
+ const searchRequest = { params: {} };
+ const requestHash = createRequestHash(searchRequest.params);
+ const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';
+ const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
+ const isStored = false;
+ const name = 'my saved background search session';
+ const created = new Date().toISOString();
+ const expires = new Date().toISOString();
+
+ await service.trackId(
+ searchRequest,
+ searchId,
+ { sessionId, isStored },
+ { savedObjectsClient }
+ );
+
+ expect(savedObjectsClient.update).not.toHaveBeenCalled();
+
+ await service.save(sessionId, { name, created, expires }, { savedObjectsClient });
+
+ expect(savedObjectsClient.create).toHaveBeenCalledWith(
+ BACKGROUND_SESSION_TYPE,
+ {
+ name,
+ created,
+ expires,
+ initialState: {},
+ restoreState: {},
+ status: BackgroundSessionStatus.IN_PROGRESS,
+ idMapping: { [requestHash]: searchId },
+ },
+ { id: sessionId }
+ );
+ });
+
+ it('updates saved object when `isStored` is `true`', async () => {
+ const searchRequest = { params: {} };
+ const requestHash = createRequestHash(searchRequest.params);
+ const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';
+ const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
+ const isStored = true;
+
+ await service.trackId(
+ searchRequest,
+ searchId,
+ { sessionId, isStored },
+ { savedObjectsClient }
+ );
+
+ expect(savedObjectsClient.update).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId, {
+ idMapping: { [requestHash]: searchId },
+ });
+ });
+ });
+
+ describe('getId', () => {
+ it('throws if `sessionId` is not provided', () => {
+ const searchRequest = { params: {} };
+
+ expect(() =>
+ service.getId(searchRequest, {}, { savedObjectsClient })
+ ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`);
+ });
+
+ it('throws if there is not a saved object', () => {
+ const searchRequest = { params: {} };
+ const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
+
+ expect(() =>
+ service.getId(searchRequest, { sessionId, isStored: false }, { savedObjectsClient })
+ ).rejects.toMatchInlineSnapshot(
+ `[Error: Cannot get search ID from a session that is not stored]`
+ );
+ });
+
+ it('throws if not restoring a saved session', () => {
+ const searchRequest = { params: {} };
+ const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
+
+ expect(() =>
+ service.getId(
+ searchRequest,
+ { sessionId, isStored: true, isRestore: false },
+ { savedObjectsClient }
+ )
+ ).rejects.toMatchInlineSnapshot(
+ `[Error: Get search ID is only supported when restoring a session]`
+ );
+ });
+
+ it('returns the search ID from the saved object ID mapping', async () => {
+ const searchRequest = { params: {} };
+ const requestHash = createRequestHash(searchRequest.params);
+ const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';
+ const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
+ const mockSession = {
+ id: 'd7170a35-7e2c-48d6-8dec-9a056721b489',
+ type: BACKGROUND_SESSION_TYPE,
+ attributes: {
+ name: 'my_name',
+ idMapping: { [requestHash]: searchId },
+ },
+ references: [],
+ };
+ savedObjectsClient.get.mockResolvedValue(mockSession);
+
+ const id = await service.getId(
+ searchRequest,
+ { sessionId, isStored: true, isRestore: true },
+ { savedObjectsClient }
+ );
+
+ expect(id).toBe(searchId);
+ });
+ });
+});
diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts
new file mode 100644
index 0000000000000..eca5f428b8555
--- /dev/null
+++ b/src/plugins/data/server/search/session/session_service.ts
@@ -0,0 +1,204 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { CoreStart, KibanaRequest, SavedObjectsClientContract } from 'kibana/server';
+import {
+ BackgroundSessionSavedObjectAttributes,
+ IKibanaSearchRequest,
+ ISearchOptions,
+ SearchSessionFindOptions,
+ BackgroundSessionStatus,
+} from '../../../common';
+import { BACKGROUND_SESSION_TYPE } from '../../saved_objects';
+import { createRequestHash } from './utils';
+
+const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000;
+
+export interface BackgroundSessionDependencies {
+ savedObjectsClient: SavedObjectsClientContract;
+}
+
+export type ISearchSessionClient = ReturnType<
+ ReturnType
+>;
+
+export class BackgroundSessionService {
+ /**
+ * Map of sessionId to { [requestHash]: searchId }
+ * @private
+ */
+ private sessionSearchMap = new Map>();
+
+ constructor() {}
+
+ public setup = () => {};
+
+ public start = (core: CoreStart) => {
+ return {
+ asScoped: this.asScopedProvider(core),
+ };
+ };
+
+ public stop = () => {
+ this.sessionSearchMap.clear();
+ };
+
+ // TODO: Generate the `userId` from the realm type/realm name/username
+ public save = async (
+ sessionId: string,
+ {
+ name,
+ created = new Date().toISOString(),
+ expires = new Date(Date.now() + DEFAULT_EXPIRATION).toISOString(),
+ status = BackgroundSessionStatus.IN_PROGRESS,
+ initialState = {},
+ restoreState = {},
+ }: Partial,
+ { savedObjectsClient }: BackgroundSessionDependencies
+ ) => {
+ if (!name) throw new Error('Name is required');
+
+ // Get the mapping of request hash/search ID for this session
+ const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map();
+ const idMapping = Object.fromEntries(searchMap.entries());
+ const attributes = { name, created, expires, status, initialState, restoreState, idMapping };
+ const session = await savedObjectsClient.create(
+ BACKGROUND_SESSION_TYPE,
+ attributes,
+ { id: sessionId }
+ );
+
+ // Clear out the entries for this session ID so they don't get saved next time
+ this.sessionSearchMap.delete(sessionId);
+
+ return session;
+ };
+
+ // TODO: Throw an error if this session doesn't belong to this user
+ public get = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => {
+ return savedObjectsClient.get(
+ BACKGROUND_SESSION_TYPE,
+ sessionId
+ );
+ };
+
+ // TODO: Throw an error if this session doesn't belong to this user
+ public find = (
+ options: SearchSessionFindOptions,
+ { savedObjectsClient }: BackgroundSessionDependencies
+ ) => {
+ return savedObjectsClient.find({
+ ...options,
+ type: BACKGROUND_SESSION_TYPE,
+ });
+ };
+
+ // TODO: Throw an error if this session doesn't belong to this user
+ public update = (
+ sessionId: string,
+ attributes: Partial,
+ { savedObjectsClient }: BackgroundSessionDependencies
+ ) => {
+ return savedObjectsClient.update(
+ BACKGROUND_SESSION_TYPE,
+ sessionId,
+ attributes
+ );
+ };
+
+ // TODO: Throw an error if this session doesn't belong to this user
+ public delete = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => {
+ return savedObjectsClient.delete(BACKGROUND_SESSION_TYPE, sessionId);
+ };
+
+ /**
+ * Tracks the given search request/search ID in the saved session (if it exists). Otherwise, just
+ * store it in memory until a saved session exists.
+ * @internal
+ */
+ public trackId = async (
+ searchRequest: IKibanaSearchRequest,
+ searchId: string,
+ { sessionId, isStored }: ISearchOptions,
+ deps: BackgroundSessionDependencies
+ ) => {
+ if (!sessionId || !searchId) return;
+ const requestHash = createRequestHash(searchRequest.params);
+
+ // If there is already a saved object for this session, update it to include this request/ID.
+ // Otherwise, just update the in-memory mapping for this session for when the session is saved.
+ if (isStored) {
+ const attributes = { idMapping: { [requestHash]: searchId } };
+ await this.update(sessionId, attributes, deps);
+ } else {
+ const map = this.sessionSearchMap.get(sessionId) ?? new Map();
+ map.set(requestHash, searchId);
+ this.sessionSearchMap.set(sessionId, map);
+ }
+ };
+
+ /**
+ * Look up an existing search ID that matches the given request in the given session so that the
+ * request can continue rather than restart.
+ * @internal
+ */
+ public getId = async (
+ searchRequest: IKibanaSearchRequest,
+ { sessionId, isStored, isRestore }: ISearchOptions,
+ deps: BackgroundSessionDependencies
+ ) => {
+ if (!sessionId) {
+ throw new Error('Session ID is required');
+ } else if (!isStored) {
+ throw new Error('Cannot get search ID from a session that is not stored');
+ } else if (!isRestore) {
+ throw new Error('Get search ID is only supported when restoring a session');
+ }
+
+ const session = await this.get(sessionId, deps);
+ const requestHash = createRequestHash(searchRequest.params);
+ if (!session.attributes.idMapping.hasOwnProperty(requestHash)) {
+ throw new Error('No search ID in this session matching the given search request');
+ }
+
+ return session.attributes.idMapping[requestHash];
+ };
+
+ public asScopedProvider = ({ savedObjects }: CoreStart) => {
+ return (request: KibanaRequest) => {
+ const savedObjectsClient = savedObjects.getScopedClient(request, {
+ includedHiddenTypes: [BACKGROUND_SESSION_TYPE],
+ });
+ const deps = { savedObjectsClient };
+ return {
+ save: (sessionId: string, attributes: Partial) =>
+ this.save(sessionId, attributes, deps),
+ get: (sessionId: string) => this.get(sessionId, deps),
+ find: (options: SearchSessionFindOptions) => this.find(options, deps),
+ update: (sessionId: string, attributes: Partial) =>
+ this.update(sessionId, attributes, deps),
+ delete: (sessionId: string) => this.delete(sessionId, deps),
+ trackId: (searchRequest: IKibanaSearchRequest, searchId: string, options: ISearchOptions) =>
+ this.trackId(searchRequest, searchId, options, deps),
+ getId: (searchRequest: IKibanaSearchRequest, options: ISearchOptions) =>
+ this.getId(searchRequest, options, deps),
+ };
+ };
+ };
+}
diff --git a/src/plugins/data/server/search/session/utils.test.ts b/src/plugins/data/server/search/session/utils.test.ts
new file mode 100644
index 0000000000000..d190f892a7f84
--- /dev/null
+++ b/src/plugins/data/server/search/session/utils.test.ts
@@ -0,0 +1,37 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { createRequestHash } from './utils';
+
+describe('data/search/session utils', () => {
+ describe('createRequestHash', () => {
+ it('ignores `preference`', () => {
+ const request = {
+ foo: 'bar',
+ };
+
+ const withPreference = {
+ ...request,
+ preference: 1234,
+ };
+
+ expect(createRequestHash(request)).toEqual(createRequestHash(withPreference));
+ });
+ });
+});
diff --git a/src/plugins/data/server/search/session/utils.ts b/src/plugins/data/server/search/session/utils.ts
new file mode 100644
index 0000000000000..c3332f80b6e3f
--- /dev/null
+++ b/src/plugins/data/server/search/session/utils.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { createHash } from 'crypto';
+
+/**
+ * Generate the hash for this request so that, in the future, this hash can be used to look up
+ * existing search IDs for this request. Ignores the `preference` parameter since it generally won't
+ * match from one request to another identical request.
+ */
+export function createRequestHash(keys: Record) {
+ const { preference, ...params } = keys;
+ return createHash(`sha256`).update(JSON.stringify(params)).digest('hex');
+}
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index ce66610edf880..47e17c26398d3 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -507,7 +507,7 @@ export interface IFieldType {
// (undocumented)
count?: number;
// (undocumented)
- customName?: string;
+ customLabel?: string;
// (undocumented)
displayName?: string;
// (undocumented)
@@ -612,7 +612,7 @@ export class IndexPattern implements IIndexPattern {
// (undocumented)
getFieldAttrs: () => {
[x: string]: {
- customName: string;
+ customLabel: string;
};
};
// (undocumented)
@@ -753,6 +753,8 @@ export class IndexPatternsService implements Plugin_3(
* updating the "value" state.
*/
const formatInputValue = useCallback(
- (inputValue: unknown): T => {
+ (inputValue: unknown): U => {
const isEmptyString = typeof inputValue === 'string' && inputValue.trim() === '';
if (isEmptyString || !formatters) {
- return inputValue as T;
+ return inputValue as U;
}
const formData = __getFormData$().value;
- return formatters.reduce((output, formatter) => formatter(output, formData), inputValue) as T;
+ return formatters.reduce((output, formatter) => formatter(output, formData), inputValue) as U;
},
[formatters, __getFormData$]
);
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx
index 4b63eb5c56fd1..8dd95adf00cc8 100644
--- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx
+++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx
@@ -151,9 +151,9 @@ const editDescription = i18n.translate(
{ defaultMessage: 'Edit' }
);
-const customNameDescription = i18n.translate(
- 'indexPatternManagement.editIndexPattern.fields.table.customNameTooltip',
- { defaultMessage: 'A custom name for the field.' }
+const labelDescription = i18n.translate(
+ 'indexPatternManagement.editIndexPattern.fields.table.customLabelTooltip',
+ { defaultMessage: 'A custom label for the field.' }
);
interface IndexedFieldProps {
@@ -197,11 +197,11 @@ export class Table extends PureComponent {
/>
) : null}
- {field.customName && field.customName !== field.name ? (
+ {field.customLabel && field.customLabel !== field.name ? (
-
+
- {field.customName}
+ {field.customLabel}
diff --git a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap
index babfbbfc2a763..29cbec38a5982 100644
--- a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap
+++ b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap
@@ -54,15 +54,15 @@ exports[`FieldEditor should render create new scripted field correctly 1`] = `
}
- label="Custom name"
+ label="Custom label"
>
@@ -294,15 +294,15 @@ exports[`FieldEditor should render edit scripted field correctly 1`] = `
}
- label="Custom name"
+ label="Custom label"
>
}
- label="Custom name"
+ label="Custom label"
>
}
- label="Custom name"
+ label="Custom label"
>
}
- label="Custom name"
+ label="Custom label"
>
{
expect(component).toMatchSnapshot();
});
- it('should display and update a customName correctly', async () => {
+ it('should display and update a custom label correctly', async () => {
let testField = ({
name: 'test',
format: new Format(),
lang: undefined,
type: 'string',
- customName: 'Test',
+ customLabel: 'Test',
} as unknown) as IndexPatternField;
fieldList.push(testField);
indexPattern.fields.getByName = (name) => {
@@ -219,14 +219,14 @@ describe('FieldEditor', () => {
await new Promise((resolve) => process.nextTick(resolve));
component.update();
- const input = findTestSubject(component, 'editorFieldCustomName');
+ const input = findTestSubject(component, 'editorFieldCustomLabel');
expect(input.props().value).toBe('Test');
input.simulate('change', { target: { value: 'new Test' } });
const saveBtn = findTestSubject(component, 'fieldSaveButton');
await saveBtn.simulate('click');
await new Promise((resolve) => process.nextTick(resolve));
- expect(testField.customName).toEqual('new Test');
+ expect(testField.customLabel).toEqual('new Test');
});
it('should show deprecated lang warning', async () => {
diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx
index 97d30d88e018c..29a87a65fdff7 100644
--- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx
+++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx
@@ -126,7 +126,7 @@ export interface FieldEditorState {
errors?: string[];
format: any;
spec: IndexPatternField['spec'];
- customName: string;
+ customLabel: string;
}
export interface FieldEdiorProps {
@@ -167,7 +167,7 @@ export class FieldEditor extends PureComponent
}
>
{
- this.setState({ customName: e.target.value });
+ this.setState({ customLabel: e.target.value });
}}
/>
@@ -802,7 +802,7 @@ export class FieldEditor extends PureComponent {
const field = this.state.spec;
const { indexPattern } = this.props;
- const { fieldFormatId, fieldFormatParams, customName } = this.state;
+ const { fieldFormatId, fieldFormatParams, customLabel } = this.state;
if (field.scripted) {
this.setState({
@@ -843,8 +843,8 @@ export class FieldEditor extends PureComponent
{this.renderScriptingPanels()}
{this.renderName()}
- {this.renderCustomName()}
+ {this.renderCustomLabel()}
{this.renderLanguage()}
{this.renderType()}
{this.renderTypeConflict()}
diff --git a/src/plugins/inspector/common/adapters/data/data_adapter.ts b/src/plugins/inspector/common/adapters/data/data_adapter.ts
index 34e6c278c693f..a21aa7db39145 100644
--- a/src/plugins/inspector/common/adapters/data/data_adapter.ts
+++ b/src/plugins/inspector/common/adapters/data/data_adapter.ts
@@ -20,7 +20,7 @@
import { EventEmitter } from 'events';
import { TabularCallback, TabularHolder, TabularLoaderOptions } from './types';
-class DataAdapter extends EventEmitter {
+export class DataAdapter extends EventEmitter {
private tabular?: TabularCallback;
private tabularOptions?: TabularLoaderOptions;
@@ -38,5 +38,3 @@ class DataAdapter extends EventEmitter {
return Promise.resolve(this.tabular()).then((data) => ({ data, options }));
}
}
-
-export { DataAdapter };
diff --git a/src/plugins/inspector/common/adapters/data/data_adapters.test.ts b/src/plugins/inspector/common/adapters/data/data_adapters.test.ts
index 287024ca1b59e..7cc52807548f0 100644
--- a/src/plugins/inspector/common/adapters/data/data_adapters.test.ts
+++ b/src/plugins/inspector/common/adapters/data/data_adapters.test.ts
@@ -35,33 +35,37 @@ describe('DataAdapter', () => {
});
it('should call the provided callback and resolve with its value', async () => {
- const spy = jest.fn(() => 'foo');
+ const data = { columns: [], rows: [] };
+ const spy = jest.fn(() => data);
adapter.setTabularLoader(spy);
expect(spy).not.toBeCalled();
const result = await adapter.getTabular();
expect(spy).toBeCalled();
- expect(result.data).toBe('foo');
+ expect(result.data).toBe(data);
});
it('should pass through options specified via setTabularLoader', async () => {
- adapter.setTabularLoader(() => 'foo', { returnsFormattedValues: true });
+ const data = { columns: [], rows: [] };
+ adapter.setTabularLoader(() => data, { returnsFormattedValues: true });
const result = await adapter.getTabular();
expect(result.options).toEqual({ returnsFormattedValues: true });
});
it('should return options set when starting loading data', async () => {
- adapter.setTabularLoader(() => 'foo', { returnsFormattedValues: true });
+ const data = { columns: [], rows: [] };
+ adapter.setTabularLoader(() => data, { returnsFormattedValues: true });
const waitForResult = adapter.getTabular();
- adapter.setTabularLoader(() => 'bar', { returnsFormattedValues: false });
+ adapter.setTabularLoader(() => data, { returnsFormattedValues: false });
const result = await waitForResult;
expect(result.options).toEqual({ returnsFormattedValues: true });
});
});
it('should emit a "tabular" event when a new tabular loader is specified', () => {
+ const data = { columns: [], rows: [] };
const spy = jest.fn();
adapter.once('change', spy);
- adapter.setTabularLoader(() => 42);
+ adapter.setTabularLoader(() => data);
expect(spy).toBeCalled();
});
});
diff --git a/src/plugins/inspector/common/adapters/data/formatted_data.ts b/src/plugins/inspector/common/adapters/data/formatted_data.ts
index c752e8670aca3..08c956f27d011 100644
--- a/src/plugins/inspector/common/adapters/data/formatted_data.ts
+++ b/src/plugins/inspector/common/adapters/data/formatted_data.ts
@@ -17,8 +17,6 @@
* under the License.
*/
-class FormattedData {
+export class FormattedData {
constructor(public readonly raw: any, public readonly formatted: any) {}
}
-
-export { FormattedData };
diff --git a/src/plugins/inspector/common/adapters/data/index.ts b/src/plugins/inspector/common/adapters/data/index.ts
index 920e298ab455f..a8b1abcd8cd7e 100644
--- a/src/plugins/inspector/common/adapters/data/index.ts
+++ b/src/plugins/inspector/common/adapters/data/index.ts
@@ -17,5 +17,6 @@
* under the License.
*/
-export { FormattedData } from './formatted_data';
-export { DataAdapter } from './data_adapter';
+export * from './data_adapter';
+export * from './formatted_data';
+export * from './types';
diff --git a/src/plugins/inspector/common/adapters/data/types.ts b/src/plugins/inspector/common/adapters/data/types.ts
index 1c7b17c143eca..040724f4ae36e 100644
--- a/src/plugins/inspector/common/adapters/data/types.ts
+++ b/src/plugins/inspector/common/adapters/data/types.ts
@@ -17,8 +17,25 @@
* under the License.
*/
-// TODO: add a more specific TabularData type.
-export type TabularData = any;
+export interface TabularDataValue {
+ formatted: string;
+ raw: unknown;
+}
+
+export interface TabularDataColumn {
+ name: string;
+ field: string;
+ filter?: (value: TabularDataValue) => void;
+ filterOut?: (value: TabularDataValue) => void;
+}
+
+export type TabularDataRow = Record;
+
+export interface TabularData {
+ columns: TabularDataColumn[];
+ rows: TabularDataRow[];
+}
+
export type TabularCallback = () => TabularData | Promise;
export interface TabularHolder {
diff --git a/src/plugins/inspector/common/adapters/index.ts b/src/plugins/inspector/common/adapters/index.ts
index 1e7a44a2c60b1..0c6319a2905a8 100644
--- a/src/plugins/inspector/common/adapters/index.ts
+++ b/src/plugins/inspector/common/adapters/index.ts
@@ -17,12 +17,6 @@
* under the License.
*/
-export { Adapters } from './types';
-export { DataAdapter, FormattedData } from './data';
-export {
- RequestAdapter,
- RequestStatistic,
- RequestStatistics,
- RequestStatus,
- RequestResponder,
-} from './request';
+export * from './data';
+export * from './request';
+export * from './types';
diff --git a/src/plugins/inspector/common/adapters/request/request_adapter.ts b/src/plugins/inspector/common/adapters/request/request_adapter.ts
index af10d1b77b16d..5f5728e1cf331 100644
--- a/src/plugins/inspector/common/adapters/request/request_adapter.ts
+++ b/src/plugins/inspector/common/adapters/request/request_adapter.ts
@@ -29,7 +29,7 @@ import { Request, RequestParams, RequestStatus } from './types';
* instead it offers a generic API to log requests of any kind.
* @extends EventEmitter
*/
-class RequestAdapter extends EventEmitter {
+export class RequestAdapter extends EventEmitter {
private requests: Map;
constructor() {
@@ -78,5 +78,3 @@ class RequestAdapter extends EventEmitter {
this.emit('change');
}
}
-
-export { RequestAdapter };
diff --git a/src/plugins/inspector/common/adapters/types.ts b/src/plugins/inspector/common/adapters/types.ts
index 362c69e299c9d..b51c3e56c749f 100644
--- a/src/plugins/inspector/common/adapters/types.ts
+++ b/src/plugins/inspector/common/adapters/types.ts
@@ -17,9 +17,14 @@
* under the License.
*/
+import type { DataAdapter } from './data';
+import type { RequestAdapter } from './request';
+
/**
* The interface that the adapters used to open an inspector have to fullfill.
*/
export interface Adapters {
+ data?: DataAdapter;
+ requests?: RequestAdapter;
[key: string]: any;
}
diff --git a/src/plugins/inspector/common/index.ts b/src/plugins/inspector/common/index.ts
index 06ab36a577d98..c5755b22095dc 100644
--- a/src/plugins/inspector/common/index.ts
+++ b/src/plugins/inspector/common/index.ts
@@ -17,4 +17,17 @@
* under the License.
*/
-export * from './adapters';
+export {
+ Adapters,
+ DataAdapter,
+ FormattedData,
+ RequestAdapter,
+ RequestStatistic,
+ RequestStatistics,
+ RequestStatus,
+ RequestResponder,
+ TabularData,
+ TabularDataColumn,
+ TabularDataRow,
+ TabularDataValue,
+} from './adapters';
diff --git a/src/plugins/inspector/public/test/is_available.test.ts b/src/plugins/inspector/public/test/is_available.test.ts
index 0604129a0734a..c38d9d7a3f825 100644
--- a/src/plugins/inspector/public/test/is_available.test.ts
+++ b/src/plugins/inspector/public/test/is_available.test.ts
@@ -18,8 +18,7 @@
*/
import { inspectorPluginMock } from '../mocks';
-import { DataAdapter } from '../../common/adapters/data/data_adapter';
-import { RequestAdapter } from '../../common/adapters/request/request_adapter';
+import { DataAdapter, RequestAdapter } from '../../common/adapters';
const adapter1 = new DataAdapter();
const adapter2 = new RequestAdapter();
diff --git a/src/plugins/inspector/public/views/data/components/data_view.tsx b/src/plugins/inspector/public/views/data/components/data_view.tsx
index 100fa7787321c..324094d8f93d0 100644
--- a/src/plugins/inspector/public/views/data/components/data_view.tsx
+++ b/src/plugins/inspector/public/views/data/components/data_view.tsx
@@ -35,7 +35,7 @@ import { Adapters } from '../../../../common';
import {
TabularLoaderOptions,
TabularData,
- TabularCallback,
+ TabularHolder,
} from '../../../../common/adapters/data/types';
import { IUiSettingsClient } from '../../../../../../core/public';
import { withKibana, KibanaReactContextValue } from '../../../../../kibana_react/public';
@@ -44,7 +44,7 @@ interface DataViewComponentState {
tabularData: TabularData | null;
tabularOptions: TabularLoaderOptions;
adapters: Adapters;
- tabularPromise: TabularCallback | null;
+ tabularPromise: Promise | null;
}
interface DataViewComponentProps extends InspectorViewProps {
@@ -73,7 +73,7 @@ class DataViewComponent extends Component string;
+
export interface DataViewColumn {
name: string;
field: string;
- sortable: (item: DataViewRow) => string | number;
+ sortable: (item: TabularDataRow) => string | number;
render: DataViewColumnRender;
}
-type DataViewColumnRender = (value: string, _item: DataViewRow) => string;
-
-export interface DataViewRow {
- [fields: string]: {
- formatted: string;
- raw: any;
- };
-}
+export type DataViewRow = TabularDataRow;
diff --git a/src/plugins/inspector/public/views/requests/components/requests_view.tsx b/src/plugins/inspector/public/views/requests/components/requests_view.tsx
index 7762689daf4e6..e1879f7a6b6c8 100644
--- a/src/plugins/inspector/public/views/requests/components/requests_view.tsx
+++ b/src/plugins/inspector/public/views/requests/components/requests_view.tsx
@@ -31,7 +31,7 @@ import { RequestDetails } from './request_details';
interface RequestSelectorState {
requests: Request[];
- request: Request;
+ request: Request | null;
}
export class RequestsViewComponent extends Component {
@@ -43,9 +43,9 @@ export class RequestsViewComponent extends Component {
- const requests = this.props.adapters.requests.getRequests();
+ const requests = this.props.adapters.requests!.getRequests();
const newState = { requests } as RequestSelectorState;
- if (!requests.includes(this.state.request)) {
+ if (!this.state.request || !requests.includes(this.state.request)) {
newState.request = requests.length ? requests[0] : null;
}
this.setState(newState);
@@ -69,7 +69,7 @@ export class RequestsViewComponent extends Component
-
-
+ {this.state.request && (
+ <>
+
+
+ >
+ )}
{this.state.request && this.state.request.description && (
diff --git a/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx
index 81101f3180738..48e5ee3c87e37 100644
--- a/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx
+++ b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx
@@ -97,11 +97,11 @@ test('context receives stateContainer', () => {
const { Provider, context } = createStateContainerReactHelpers();
ReactDOM.render(
- /* eslint-disable no-shadow */
+ /* eslint-disable @typescript-eslint/no-shadow */
{(stateContainer) => stateContainer.get().foo}
,
- /* eslint-enable no-shadow */
+ /* eslint-enable @typescript-eslint/no-shadow */
container
);
@@ -116,7 +116,7 @@ describe('hooks', () => {
const stateContainer = createStateContainer({ foo: 'bar' });
const { Provider, useContainer } = createStateContainerReactHelpers();
const Demo: React.FC<{}> = () => {
- // eslint-disable-next-line no-shadow
+ // eslint-disable-next-line @typescript-eslint/no-shadow
const stateContainer = useContainer();
return <>{stateContainer.get().foo}>;
};
diff --git a/src/plugins/kibana_utils/demos/state_sync/url.ts b/src/plugins/kibana_utils/demos/state_sync/url.ts
index e8e63eefe866c..f7a66e79b8170 100644
--- a/src/plugins/kibana_utils/demos/state_sync/url.ts
+++ b/src/plugins/kibana_utils/demos/state_sync/url.ts
@@ -56,9 +56,9 @@ export const result = Promise.resolve()
});
function withDefaultState(
- // eslint-disable-next-line no-shadow
+ // eslint-disable-next-line @typescript-eslint/no-shadow
stateContainer: BaseStateContainer,
- // eslint-disable-next-line no-shadow
+ // eslint-disable-next-line @typescript-eslint/no-shadow
defaultState: State
): INullableBaseStateContainer {
return {
diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts
index 4b2b2bd99911b..f96c243e82f89 100644
--- a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts
+++ b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts
@@ -354,7 +354,7 @@ describe('state_sync', () => {
function withDefaultState(
stateContainer: BaseStateContainer,
- // eslint-disable-next-line no-shadow
+ // eslint-disable-next-line @typescript-eslint/no-shadow
defaultState: State
): INullableBaseStateContainer {
return {
diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts
index 2cd06f13a8855..c9e2f22fa19aa 100644
--- a/src/plugins/telemetry_collection_manager/server/plugin.ts
+++ b/src/plugins/telemetry_collection_manager/server/plugin.ts
@@ -289,9 +289,9 @@ export class TelemetryCollectionManagerPlugin
return stats.map((stat) => {
const license = licenses[stat.cluster_uuid];
return {
+ collectionSource: collection.title,
...(license ? { license } : {}),
...stat,
- collectionSource: collection.title,
};
});
}
diff --git a/src/plugins/ui_actions/public/public.api.md b/src/plugins/ui_actions/public/public.api.md
index 3e40c94e116fb..3a14f49169e09 100644
--- a/src/plugins/ui_actions/public/public.api.md
+++ b/src/plugins/ui_actions/public/public.api.md
@@ -8,6 +8,7 @@ import { CoreSetup } from 'src/core/public';
import { CoreStart } from 'src/core/public';
import { EnvironmentMode } from '@kbn/config';
import { EuiContextMenuPanelDescriptor } from '@elastic/eui';
+import { EventEmitter } from 'events';
import { Observable } from 'rxjs';
import { PackageInfo } from '@kbn/config';
import { Plugin } from 'src/core/public';
diff --git a/src/plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx b/src/plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx
index ae3da8e203a57..a316a087c8bcb 100644
--- a/src/plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx
+++ b/src/plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx
@@ -38,7 +38,6 @@ function HasExtendedBoundsParamEditor(props: AggParamEditorProps) {
setValue(value && agg.params.min_doc_count);
}
- /* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [agg.params.min_doc_count, setValue, value]);
return (
diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.js
index a9ec431e9d940..d3eac891c81f4 100644
--- a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.js
+++ b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.js
@@ -58,12 +58,8 @@ export function KbnAggTable(config, RecursionHelper) {
};
self.toCsv = function (formatted) {
- const rows = formatted ? $scope.rows : $scope.table.rows;
- const columns = formatted ? [...$scope.formattedColumns] : [...$scope.table.columns];
-
- if ($scope.splitRow && formatted) {
- columns.unshift($scope.splitRow);
- }
+ const rows = $scope.rows;
+ const columns = $scope.formattedColumns;
const nonAlphaNumRE = /[^a-zA-Z0-9]/;
const allDoubleQuoteRE = /"/g;
@@ -77,7 +73,7 @@ export function KbnAggTable(config, RecursionHelper) {
return val;
}
- let csvRows = [];
+ const csvRows = [];
for (const row of rows) {
const rowArray = [];
for (const col of columns) {
@@ -86,15 +82,11 @@ export function KbnAggTable(config, RecursionHelper) {
formatted && col.formatter ? escape(col.formatter.convert(value)) : escape(value);
rowArray.push(formattedValue);
}
- csvRows = [...csvRows, rowArray];
+ csvRows.push(rowArray);
}
// add the columns to the rows
- csvRows.unshift(
- columns.map(function (col) {
- return escape(formatted ? col.title : col.name);
- })
- );
+ csvRows.unshift(columns.map(({ title }) => escape(title)));
return csvRows
.map(function (row) {
@@ -112,7 +104,6 @@ export function KbnAggTable(config, RecursionHelper) {
if (!table) {
$scope.rows = null;
$scope.formattedColumns = null;
- $scope.splitRow = null;
return;
}
@@ -122,19 +113,12 @@ export function KbnAggTable(config, RecursionHelper) {
if (typeof $scope.dimensions === 'undefined') return;
- const { buckets, metrics, splitColumn, splitRow } = $scope.dimensions;
+ const { buckets, metrics } = $scope.dimensions;
$scope.formattedColumns = table.columns
.map(function (col, i) {
const isBucket = buckets.find((bucket) => bucket.accessor === i);
- const isSplitColumn = splitColumn
- ? splitColumn.find((splitColumn) => splitColumn.accessor === i)
- : undefined;
- const isSplitRow = splitRow
- ? splitRow.find((splitRow) => splitRow.accessor === i)
- : undefined;
- const dimension =
- isBucket || isSplitColumn || metrics.find((metric) => metric.accessor === i);
+ const dimension = isBucket || metrics.find((metric) => metric.accessor === i);
const formatter = dimension
? getFormatService().deserialize(dimension.format)
@@ -147,10 +131,6 @@ export function KbnAggTable(config, RecursionHelper) {
filterable: !!isBucket,
};
- if (isSplitRow) {
- $scope.splitRow = formattedColumn;
- }
-
if (!dimension) return;
const last = i === table.columns.length - 1;
diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js
index c93fb4f8bd568..d97ef374def93 100644
--- a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js
+++ b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js
@@ -262,14 +262,12 @@ describe('Table Vis - AggTable Directive', function () {
const $tableScope = $el.isolateScope();
const aggTable = $tableScope.aggTable;
- $tableScope.table = {
- columns: [
- { id: 'a', name: 'one' },
- { id: 'b', name: 'two' },
- { id: 'c', name: 'with double-quotes(")' },
- ],
- rows: [{ a: 1, b: 2, c: '"foobar"' }],
- };
+ $tableScope.rows = [{ a: 1, b: 2, c: '"foobar"' }];
+ $tableScope.formattedColumns = [
+ { id: 'a', title: 'one' },
+ { id: 'b', title: 'two' },
+ { id: 'c', title: 'with double-quotes(")' },
+ ];
expect(aggTable.toCsv()).toBe(
'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n'
@@ -455,14 +453,12 @@ describe('Table Vis - AggTable Directive', function () {
const aggTable = $tableScope.aggTable;
const saveAs = sinon.stub(aggTable, '_saveAs');
- $tableScope.table = {
- columns: [
- { id: 'a', name: 'one' },
- { id: 'b', name: 'two' },
- { id: 'c', name: 'with double-quotes(")' },
- ],
- rows: [{ a: 1, b: 2, c: '"foobar"' }],
- };
+ $tableScope.rows = [{ a: 1, b: 2, c: '"foobar"' }];
+ $tableScope.formattedColumns = [
+ { id: 'a', title: 'one' },
+ { id: 'b', title: 'two' },
+ { id: 'c', title: 'with double-quotes(")' },
+ ];
aggTable.csv.filename = 'somefilename.csv';
aggTable.exportAsCsv();
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js
index 83ddc23648ad3..feda9fd239a66 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/metric_select.js
@@ -23,10 +23,10 @@ import { includes } from 'lodash';
import { injectI18n } from '@kbn/i18n/react';
import { EuiComboBox } from '@elastic/eui';
import { calculateSiblings } from '../lib/calculate_siblings';
-import { calculateLabel } from '../../../../../../plugins/vis_type_timeseries/common/calculate_label';
-import { basicAggs } from '../../../../../../plugins/vis_type_timeseries/common/basic_aggs';
-import { toPercentileNumber } from '../../../../../../plugins/vis_type_timeseries/common/to_percentile_number';
-import { METRIC_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/metric_types';
+import { calculateLabel } from '../../../../common/calculate_label';
+import { basicAggs } from '../../../../common/basic_aggs';
+import { toPercentileNumber } from '../../../../common/to_percentile_number';
+import { METRIC_TYPES } from '../../../../common/metric_types';
function createTypeFilter(restrict, exclude) {
return (metric) => {
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js
index fb945d2606bc8..48b6f6192a93c 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js
@@ -37,7 +37,7 @@ import {
EuiFieldNumber,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { MODEL_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/model_options';
+import { MODEL_TYPES } from '../../../../common/model_options';
const DEFAULTS = {
model_type: MODEL_TYPES.UNWEIGHTED,
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js
index c63beee222b17..1969147efde9a 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js
@@ -36,7 +36,7 @@ import {
} from '@elastic/eui';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public';
-import { PANEL_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/panel_types';
+import { PANEL_TYPES } from '../../../../common/panel_types';
const isFieldTypeEnabled = (fieldRestrictions, fieldType) =>
fieldRestrictions.length ? fieldRestrictions.includes(fieldType) : true;
diff --git a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js
index 30c6d5b51d187..85f31285df69b 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js
@@ -42,11 +42,8 @@ import {
AUTO_INTERVAL,
} from './lib/get_interval';
import { i18n } from '@kbn/i18n';
-import {
- TIME_RANGE_DATA_MODES,
- TIME_RANGE_MODE_KEY,
-} from '../../../../../plugins/vis_type_timeseries/common/timerange_data_modes';
-import { PANEL_TYPES } from '../../../../../plugins/vis_type_timeseries/common/panel_types';
+import { TIME_RANGE_DATA_MODES, TIME_RANGE_MODE_KEY } from '../../../common/timerange_data_modes';
+import { PANEL_TYPES } from '../../../common/panel_types';
import { isTimerangeModeEnabled } from '../lib/check_ui_restrictions';
import { VisDataContext } from '../contexts/vis_data_context';
diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js
index 0f64c570088d7..66783f5ef2715 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js
@@ -19,7 +19,7 @@
import { set } from '@elastic/safer-lodash-set';
import _ from 'lodash';
-import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value';
+import { getLastValue } from '../../../../common/get_last_value';
import { createTickFormatter } from './tick_formatter';
import { labelDateFormatter } from './label_date_formatter';
import moment from 'moment';
diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js
index 86361afca3b12..c1d484765f4cb 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js
@@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n';
import { get } from 'lodash';
import { search } from '../../../../../../plugins/data/public';
const { parseEsInterval } = search.aggs;
-import { GTE_INTERVAL_RE } from '../../../../../../plugins/vis_type_timeseries/common/interval_regexp';
+import { GTE_INTERVAL_RE } from '../../../../common/interval_regexp';
export const AUTO_INTERVAL = 'auto';
diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js
index 146e7a4bae15a..f8b6f19ac21a2 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js
@@ -18,7 +18,7 @@
*/
import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public';
-import { METRIC_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/metric_types';
+import { METRIC_TYPES } from '../../../../common/metric_types';
export function getSupportedFieldsByMetricType(type) {
switch (type) {
diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js b/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js
index 0638c6e67f5ef..b6b99d7782762 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/lib/series_change_handler.js
@@ -19,7 +19,7 @@
import _ from 'lodash';
import { newMetricAggFn } from './new_metric_agg_fn';
-import { isBasicAgg } from '../../../../../../plugins/vis_type_timeseries/common/agg_lookup';
+import { isBasicAgg } from '../../../../common/agg_lookup';
import { handleAdd, handleChange } from './collection_actions';
export const seriesChangeHandler = (props, items) => (doc) => {
diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js
index a72c7598509a8..fe6c89ea6985b 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js
@@ -36,7 +36,7 @@ import {
EuiFieldText,
} from '@elastic/eui';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
-import { FIELD_TYPES } from '../../../../../../plugins/vis_type_timeseries/common/field_types';
+import { FIELD_TYPES } from '../../../../common/field_types';
import { STACKED_OPTIONS } from '../../visualizations/constants';
const DEFAULTS = { terms_direction: 'desc', terms_size: 10, terms_order_by: '_count' };
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js
index 47b30f9ab2711..57adecd9d598b 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js
@@ -28,7 +28,7 @@ import { VisPicker } from './vis_picker';
import { PanelConfig } from './panel_config';
import { createBrushHandler } from '../lib/create_brush_handler';
import { fetchFields } from '../lib/fetch_fields';
-import { extractIndexPatterns } from '../../../../../plugins/vis_type_timeseries/common/extract_index_patterns';
+import { extractIndexPatterns } from '../../../common/extract_index_patterns';
import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../../services';
import { CoreStartContextProvider } from '../contexts/query_input_bar_context';
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js
index 9c2b947bda08e..9742d817f7c0d 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js
@@ -28,7 +28,7 @@ import {
isGteInterval,
AUTO_INTERVAL,
} from './lib/get_interval';
-import { PANEL_TYPES } from '../../../../../plugins/vis_type_timeseries/common/panel_types';
+import { PANEL_TYPES } from '../../../common/panel_types';
const MIN_CHART_HEIGHT = 300;
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js b/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js
index c33ed02eadebd..79f5c7abca270 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js
@@ -21,7 +21,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { EuiTabs, EuiTab } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
-import { PANEL_TYPES } from '../../../../../plugins/vis_type_timeseries/common/panel_types';
+import { PANEL_TYPES } from '../../../common/panel_types';
function VisPickerItem(props) {
const { label, type, selected } = props;
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js
index 4c029f1c0d5b0..325e9c8372736 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js
@@ -23,7 +23,7 @@ import { visWithSplits } from '../../vis_with_splits';
import { createTickFormatter } from '../../lib/tick_formatter';
import _, { get, isUndefined, assign, includes } from 'lodash';
import { Gauge } from '../../../visualizations/views/gauge';
-import { getLastValue } from '../../../../../../../plugins/vis_type_timeseries/common/get_last_value';
+import { getLastValue } from '../../../../../common/get_last_value';
function getColors(props) {
const { model, visData } = props;
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js
index f37971e990c96..5fe7afe47df9b 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js
@@ -23,7 +23,7 @@ import { visWithSplits } from '../../vis_with_splits';
import { createTickFormatter } from '../../lib/tick_formatter';
import _, { get, isUndefined, assign, includes, pick } from 'lodash';
import { Metric } from '../../../visualizations/views/metric';
-import { getLastValue } from '../../../../../../../plugins/vis_type_timeseries/common/get_last_value';
+import { getLastValue } from '../../../../../common/get_last_value';
import { isBackgroundInverted } from '../../../lib/set_is_reversed';
function getColors(props) {
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js
index b44c94131348d..099dbe6639737 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/is_sortable.js
@@ -17,7 +17,7 @@
* under the License.
*/
-import { basicAggs } from '../../../../../../../plugins/vis_type_timeseries/common/basic_aggs';
+import { basicAggs } from '../../../../../common/basic_aggs';
export function isSortable(metric) {
return basicAggs.includes(metric.type);
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js
index 1341cf02202a0..92109e1a37426 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js
@@ -22,7 +22,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { RedirectAppLinks } from '../../../../../../kibana_react/public';
import { createTickFormatter } from '../../lib/tick_formatter';
-import { calculateLabel } from '../../../../../../../plugins/vis_type_timeseries/common/calculate_label';
+import { calculateLabel } from '../../../../../common/calculate_label';
import { isSortable } from './is_sortable';
import { EuiToolTip, EuiIcon } from '@elastic/eui';
import { replaceVars } from '../../lib/replace_vars';
@@ -30,7 +30,7 @@ import { fieldFormats } from '../../../../../../../plugins/data/public';
import { FormattedMessage } from '@kbn/i18n/react';
import { getFieldFormats, getCoreStart } from '../../../../services';
-import { METRIC_TYPES } from '../../../../../../../plugins/vis_type_timeseries/common/metric_types';
+import { METRIC_TYPES } from '../../../../../common/metric_types';
function getColor(rules, colorKey, value) {
let color;
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js
index 680c1c5e78ad4..039763efc78a2 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/series.js
@@ -35,7 +35,7 @@ import {
import { Split } from '../../split';
import { createTextHandler } from '../../lib/create_text_handler';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
-import { PANEL_TYPES } from '../../../../../../../plugins/vis_type_timeseries/common/panel_types';
+import { PANEL_TYPES } from '../../../../../common/panel_types';
const TimeseriesSeriesUI = injectI18n(function (props) {
const {
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js
index c12e518a9dcd3..f936710bf2b81 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js
@@ -161,6 +161,10 @@ export class TimeseriesVisualization extends Component {
const yAxis = [];
let mainDomainAdded = false;
+ const allSeriesHaveSameFormatters = seriesModel.every(
+ (seriesGroup) => seriesGroup.formatter === seriesModel[0].formatter
+ );
+
this.showToastNotification = null;
seriesModel.forEach((seriesGroup) => {
@@ -211,7 +215,7 @@ export class TimeseriesVisualization extends Component {
});
} else if (!mainDomainAdded) {
TimeseriesVisualization.addYAxis(yAxis, {
- tickFormatter: series.length === 1 ? undefined : (val) => val,
+ tickFormatter: allSeriesHaveSameFormatters ? seriesGroupTickFormatter : (val) => val,
id: yAxisIdGenerator('main'),
groupId: mainAxisGroupId,
position: model.axis_position,
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js
index e9f64c93d337f..1c2ebb8264ef3 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js
@@ -20,7 +20,7 @@
import { getCoreStart } from '../../../../services';
import { createTickFormatter } from '../../lib/tick_formatter';
import { TopN } from '../../../visualizations/views/top_n';
-import { getLastValue } from '../../../../../../../plugins/vis_type_timeseries/common/get_last_value';
+import { getLastValue } from '../../../../../common/get_last_value';
import { isBackgroundInverted } from '../../../lib/set_is_reversed';
import { replaceVars } from '../../lib/replace_vars';
import PropTypes from 'prop-types';
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js
index f583d087e60ef..27891cdbb3943 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js
@@ -21,7 +21,7 @@ import React from 'react';
import { getDisplayName } from './lib/get_display_name';
import { labelDateFormatter } from './lib/label_date_formatter';
import { last, findIndex, first } from 'lodash';
-import { calculateLabel } from '../../../../../plugins/vis_type_timeseries/common/calculate_label';
+import { calculateLabel } from '../../../common/calculate_label';
export function visWithSplits(WrappedComponent) {
function SplitVisComponent(props) {
diff --git a/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js b/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js
index 5d18c0a2f09cd..d77f2f327b30d 100644
--- a/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js
+++ b/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js
@@ -18,10 +18,7 @@
*/
import { get } from 'lodash';
-import {
- RESTRICTIONS_KEYS,
- DEFAULT_UI_RESTRICTION,
-} from '../../../../../plugins/vis_type_timeseries/common/ui_restrictions';
+import { RESTRICTIONS_KEYS, DEFAULT_UI_RESTRICTION } from '../../../common/ui_restrictions';
/**
* Generic method for checking all types of the UI Restrictions
diff --git a/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js b/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js
index e8ddb4ceb5cba..9448a29787097 100644
--- a/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js
+++ b/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js
@@ -17,7 +17,7 @@
* under the License.
*/
-import { GTE_INTERVAL_RE } from '../../../../../plugins/vis_type_timeseries/common/interval_regexp';
+import { GTE_INTERVAL_RE } from '../../../common/interval_regexp';
import { i18n } from '@kbn/i18n';
import { search } from '../../../../../plugins/data/public';
const { parseInterval } = search.aggs;
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js
index 50a2042425438..0b9e191e4e29e 100644
--- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js
+++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js
@@ -22,7 +22,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import { isBackgroundInverted, isBackgroundDark } from '../../lib/set_is_reversed';
-import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value';
+import { getLastValue } from '../../../../common/get_last_value';
import { getValueBy } from '../lib/get_value_by';
import { GaugeVis } from './gauge_vis';
import reactcss from 'reactcss';
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js
index 4c286f61720ac..7356726e6262f 100644
--- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js
+++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js
@@ -20,8 +20,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import _ from 'lodash';
-import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value';
import reactcss from 'reactcss';
+
+import { getLastValue } from '../../../../common/get_last_value';
import { calculateCoordinates } from '../lib/calculate_coordinates';
export class Metric extends Component {
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js
index 136ac2506d392..9c6e497b92dab 100644
--- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js
+++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js
@@ -19,7 +19,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value';
+import { getLastValue } from '../../../../common/get_last_value';
import { labelDateFormatter } from '../../components/lib/label_date_formatter';
import reactcss from 'reactcss';
diff --git a/src/plugins/vis_type_timeseries/server/index.ts b/src/plugins/vis_type_timeseries/server/index.ts
index 333ed0ff64fdb..1037dc81b2b17 100644
--- a/src/plugins/vis_type_timeseries/server/index.ts
+++ b/src/plugins/vis_type_timeseries/server/index.ts
@@ -43,7 +43,9 @@ export {
AbstractSearchStrategy,
ReqFacade,
} from './lib/search_strategies/strategies/abstract_search_strategy';
-// @ts-ignore
+
+export { VisPayload } from '../common/types';
+
export { DefaultSearchCapabilities } from './lib/search_strategies/default_search_capabilities';
export function plugin(initializerContext: PluginInitializerContext) {
diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts
index dc49e280a2bb7..8f87318222f2b 100644
--- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts
@@ -38,7 +38,7 @@ export async function getFields(
// removes the need to refactor many layers of dependencies on "req", and instead just augments the top
// level object passed from here. The layers should be refactored fully at some point, but for now
// this works and we are still using the New Platform services for these vis data portions.
- const reqFacade: ReqFacade = {
+ const reqFacade: ReqFacade<{}> = {
requestContext,
...request,
framework,
diff --git a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts
index 5eef2b53e2431..fcb66d2e12fd1 100644
--- a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts
@@ -64,7 +64,7 @@ export function getVisData(
// removes the need to refactor many layers of dependencies on "req", and instead just augments the top
// level object passed from here. The layers should be refactored fully at some point, but for now
// this works and we are still using the New Platform services for these vis data portions.
- const reqFacade: ReqFacade = {
+ const reqFacade: ReqFacade = {
requestContext,
...request,
framework,
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.ts
similarity index 90%
rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.js
rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.ts
index b9b7759711567..a570e02ada8d1 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.ts
@@ -17,13 +17,15 @@
* under the License.
*/
import { DefaultSearchCapabilities } from './default_search_capabilities';
+import { ReqFacade } from './strategies/abstract_search_strategy';
+import { VisPayload } from '../../../common/types';
describe('DefaultSearchCapabilities', () => {
- let defaultSearchCapabilities;
- let req;
+ let defaultSearchCapabilities: DefaultSearchCapabilities;
+ let req: ReqFacade;
beforeEach(() => {
- req = {};
+ req = {} as ReqFacade;
defaultSearchCapabilities = new DefaultSearchCapabilities(req);
});
@@ -45,13 +47,13 @@ describe('DefaultSearchCapabilities', () => {
});
test('should return Search Timezone', () => {
- defaultSearchCapabilities.request = {
+ defaultSearchCapabilities.request = ({
payload: {
timerange: {
timezone: 'UTC',
},
},
- };
+ } as unknown) as ReqFacade;
expect(defaultSearchCapabilities.searchTimezone).toEqual('UTC');
});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.ts
similarity index 69%
rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.js
rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.ts
index 02a710fef897f..73b701379aee0 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.js
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.ts
@@ -16,40 +16,43 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { Unit } from '@elastic/datemath';
import {
convertIntervalToUnit,
parseInterval,
getSuitableUnit,
} from '../vis_data/helpers/unit_to_seconds';
import { RESTRICTIONS_KEYS } from '../../../common/ui_restrictions';
+import { ReqFacade } from './strategies/abstract_search_strategy';
+import { VisPayload } from '../../../common/types';
-const getTimezoneFromRequest = (request) => {
+const getTimezoneFromRequest = (request: ReqFacade) => {
return request.payload.timerange.timezone;
};
export class DefaultSearchCapabilities {
- constructor(request, fieldsCapabilities = {}) {
- this.request = request;
- this.fieldsCapabilities = fieldsCapabilities;
- }
+ constructor(
+ public request: ReqFacade,
+ public fieldsCapabilities: Record = {}
+ ) {}
- get defaultTimeInterval() {
+ public get defaultTimeInterval() {
return null;
}
- get whiteListedMetrics() {
+ public get whiteListedMetrics() {
return this.createUiRestriction();
}
- get whiteListedGroupByFields() {
+ public get whiteListedGroupByFields() {
return this.createUiRestriction();
}
- get whiteListedTimerangeModes() {
+ public get whiteListedTimerangeModes() {
return this.createUiRestriction();
}
- get uiRestrictions() {
+ public get uiRestrictions() {
return {
[RESTRICTIONS_KEYS.WHITE_LISTED_METRICS]: this.whiteListedMetrics,
[RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS]: this.whiteListedGroupByFields,
@@ -57,36 +60,36 @@ export class DefaultSearchCapabilities {
};
}
- get searchTimezone() {
+ public get searchTimezone() {
return getTimezoneFromRequest(this.request);
}
- createUiRestriction(restrictionsObject) {
+ createUiRestriction(restrictionsObject?: Record) {
return {
'*': !restrictionsObject,
...(restrictionsObject || {}),
};
}
- parseInterval(interval) {
+ parseInterval(interval: string) {
return parseInterval(interval);
}
- getSuitableUnit(intervalInSeconds) {
+ getSuitableUnit(intervalInSeconds: string | number) {
return getSuitableUnit(intervalInSeconds);
}
- convertIntervalToUnit(intervalString, unit) {
+ convertIntervalToUnit(intervalString: string, unit: Unit) {
const parsedInterval = this.parseInterval(intervalString);
- if (parsedInterval.unit !== unit) {
+ if (parsedInterval?.unit !== unit) {
return convertIntervalToUnit(intervalString, unit);
}
return parsedInterval;
}
- getValidTimeInterval(intervalString) {
+ getValidTimeInterval(intervalString: string) {
// Default search capabilities doesn't have any restrictions for the interval string
return intervalString;
}
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts
index 66ea4f017dd90..4c3dcbd17bbd9 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts
@@ -27,10 +27,10 @@ import { DefaultSearchCapabilities } from './default_search_capabilities';
class MockSearchStrategy extends AbstractSearchStrategy {
checkForViability() {
- return {
+ return Promise.resolve({
isViable: true,
capabilities: {},
- };
+ });
}
}
@@ -65,7 +65,7 @@ describe('SearchStrategyRegister', () => {
});
test('should add a strategy if it is an instance of AbstractSearchStrategy', () => {
- const anotherSearchStrategy = new MockSearchStrategy('es');
+ const anotherSearchStrategy = new MockSearchStrategy();
const addedStrategies = registry.addStrategy(anotherSearchStrategy);
expect(addedStrategies.length).toEqual(2);
@@ -75,7 +75,7 @@ describe('SearchStrategyRegister', () => {
test('should return a MockSearchStrategy instance', async () => {
const req = {};
const indexPattern = '*';
- const anotherSearchStrategy = new MockSearchStrategy('es');
+ const anotherSearchStrategy = new MockSearchStrategy();
registry.addStrategy(anotherSearchStrategy);
const { searchStrategy, capabilities } = (await registry.getViableStrategy(req, indexPattern))!;
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 b1e21edf8b588..71461d319f2b6 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
@@ -46,16 +46,8 @@ export interface ReqFacade extends FakeRequest {
getEsShardTimeout: () => Promise;
}
-export class AbstractSearchStrategy {
- public indexType?: string;
- public additionalParams: any;
-
- constructor(type?: string, additionalParams: any = {}) {
- this.indexType = type;
- this.additionalParams = additionalParams;
- }
-
- async search(req: ReqFacade, bodies: any[], options = {}) {
+export abstract class AbstractSearchStrategy {
+ async search(req: ReqFacade, bodies: any[], indexType?: string) {
const requests: any[] = [];
const { sessionId } = req.payload;
@@ -64,15 +56,13 @@ export class AbstractSearchStrategy {
req.requestContext
.search!.search(
{
+ indexType,
params: {
...body,
- ...this.additionalParams,
},
- indexType: this.indexType,
},
{
sessionId,
- ...options,
}
)
.toPromise()
@@ -81,7 +71,18 @@ export class AbstractSearchStrategy {
return Promise.all(requests);
}
- async getFieldsForWildcard(req: ReqFacade, indexPattern: string, capabilities: any) {
+ checkForViability(
+ req: ReqFacade,
+ indexPattern: string
+ ): Promise<{ isViable: boolean; capabilities: unknown }> {
+ throw new TypeError('Must override method');
+ }
+
+ async getFieldsForWildcard(
+ req: ReqFacade,
+ indexPattern: string,
+ capabilities?: unknown
+ ) {
const { indexPatternsService } = req.pre;
return await indexPatternsService!.getFieldsForWildcard({
@@ -89,11 +90,4 @@ export class AbstractSearchStrategy {
fieldCapsOptions: { allow_no_indices: true },
});
}
-
- checkForViability(
- req: ReqFacade,
- indexPattern: string
- ): { isViable: boolean; capabilities: any } {
- throw new TypeError('Must override method');
- }
}
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts
similarity index 79%
rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js
rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts
index a9994ba3e1f75..d8ea6c9c8a526 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts
@@ -17,13 +17,15 @@
* under the License.
*/
import { DefaultSearchStrategy } from './default_search_strategy';
+import { ReqFacade } from './abstract_search_strategy';
+import { VisPayload } from '../../../../common/types';
describe('DefaultSearchStrategy', () => {
- let defaultSearchStrategy;
- let req;
+ let defaultSearchStrategy: DefaultSearchStrategy;
+ let req: ReqFacade;
beforeEach(() => {
- req = {};
+ req = {} as ReqFacade;
defaultSearchStrategy = new DefaultSearchStrategy();
});
@@ -34,8 +36,8 @@ describe('DefaultSearchStrategy', () => {
expect(defaultSearchStrategy.getFieldsForWildcard).toBeDefined();
});
- test('should check a strategy for viability', () => {
- const value = defaultSearchStrategy.checkForViability(req);
+ test('should check a strategy for viability', async () => {
+ const value = await defaultSearchStrategy.checkForViability(req);
expect(value.isViable).toBe(true);
expect(value.capabilities).toEqual({
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts
similarity index 82%
rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js
rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts
index 8e57c117637bf..e1f519456d373 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts
@@ -17,16 +17,17 @@
* under the License.
*/
-import { AbstractSearchStrategy } from './abstract_search_strategy';
+import { AbstractSearchStrategy, ReqFacade } from './abstract_search_strategy';
import { DefaultSearchCapabilities } from '../default_search_capabilities';
+import { VisPayload } from '../../../../common/types';
export class DefaultSearchStrategy extends AbstractSearchStrategy {
name = 'default';
- checkForViability(req) {
- return {
+ checkForViability(req: ReqFacade) {
+ return Promise.resolve({
isViable: true,
capabilities: new DefaultSearchCapabilities(req),
- };
+ });
}
}
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js
index 53f0b84b8ec3b..c021ba3cebc66 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js
@@ -42,14 +42,18 @@ const calculateBucketData = (timeInterval, capabilities) => {
}
// Check decimal
- if (parsedInterval.value % 1 !== 0) {
+ if (parsedInterval && parsedInterval.value % 1 !== 0) {
if (parsedInterval.unit !== 'ms') {
- const { value, unit } = convertIntervalToUnit(
+ const converted = convertIntervalToUnit(
intervalString,
ASCENDING_UNIT_ORDER[ASCENDING_UNIT_ORDER.indexOf(parsedInterval.unit) - 1]
);
- intervalString = value + unit;
+ if (converted) {
+ intervalString = converted.value + converted.unit;
+ }
+
+ intervalString = undefined;
} else {
intervalString = '1ms';
}
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.ts
similarity index 86%
rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.js
rename to src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.ts
index 5b533178949f1..278e557209a21 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.ts
@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { Unit } from '@elastic/datemath';
import {
getUnitValue,
@@ -51,22 +52,13 @@ describe('unit_to_seconds', () => {
}));
test('should not parse "gm" interval (negative)', () =>
- expect(parseInterval('gm')).toEqual({
- value: undefined,
- unit: undefined,
- }));
+ expect(parseInterval('gm')).toBeUndefined());
test('should not parse "-1d" interval (negative)', () =>
- expect(parseInterval('-1d')).toEqual({
- value: undefined,
- unit: undefined,
- }));
+ expect(parseInterval('-1d')).toBeUndefined());
test('should not parse "M" interval (negative)', () =>
- expect(parseInterval('M')).toEqual({
- value: undefined,
- unit: undefined,
- }));
+ expect(parseInterval('M')).toBeUndefined());
});
describe('convertIntervalToUnit()', () => {
@@ -95,16 +87,10 @@ describe('unit_to_seconds', () => {
}));
test('should not convert "30m" interval to "0" unit (positive)', () =>
- expect(convertIntervalToUnit('30m', 'o')).toEqual({
- value: undefined,
- unit: undefined,
- }));
+ expect(convertIntervalToUnit('30m', 'o' as Unit)).toBeUndefined());
test('should not convert "m" interval to "s" unit (positive)', () =>
- expect(convertIntervalToUnit('m', 's')).toEqual({
- value: undefined,
- unit: undefined,
- }));
+ expect(convertIntervalToUnit('m', 's')).toBeUndefined());
});
describe('getSuitableUnit()', () => {
@@ -155,8 +141,5 @@ describe('unit_to_seconds', () => {
expect(getSuitableUnit(stringValue)).toBeUndefined();
});
-
- test('should return "undefined" in case of no input value(negative)', () =>
- expect(getSuitableUnit()).toBeUndefined());
});
});
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.ts
similarity index 60%
rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.js
rename to src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.ts
index be8f1741627ba..8950e05c85d4f 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.ts
@@ -16,12 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { INTERVAL_STRING_RE } from '../../../../common/interval_regexp';
import { sortBy, isNumber } from 'lodash';
+import { Unit } from '@elastic/datemath';
+
+/** @ts-ignore */
+import { INTERVAL_STRING_RE } from '../../../../common/interval_regexp';
export const ASCENDING_UNIT_ORDER = ['ms', 's', 'm', 'h', 'd', 'w', 'M', 'y'];
-const units = {
+const units: Record = {
ms: 0.001,
s: 1,
m: 60,
@@ -32,49 +35,53 @@ const units = {
y: 86400 * 7 * 4 * 12, // Leap year?
};
-const sortedUnits = sortBy(Object.keys(units), (key) => units[key]);
+const sortedUnits = sortBy(Object.keys(units), (key: Unit) => units[key]);
-export const parseInterval = (intervalString) => {
- let value;
- let unit;
+interface ParsedInterval {
+ value: number;
+ unit: Unit;
+}
+export const parseInterval = (intervalString: string): ParsedInterval | undefined => {
if (intervalString) {
const matches = intervalString.match(INTERVAL_STRING_RE);
if (matches) {
- value = Number(matches[1]);
- unit = matches[2];
+ return {
+ value: Number(matches[1]),
+ unit: matches[2] as Unit,
+ };
}
}
-
- return { value, unit };
};
-export const convertIntervalToUnit = (intervalString, newUnit) => {
+export const convertIntervalToUnit = (
+ intervalString: string,
+ newUnit: Unit
+): ParsedInterval | undefined => {
const parsedInterval = parseInterval(intervalString);
- let value;
- let unit;
- if (parsedInterval.value && units[newUnit]) {
- value = Number(
- ((parsedInterval.value * units[parsedInterval.unit]) / units[newUnit]).toFixed(2)
- );
- unit = newUnit;
+ if (parsedInterval && units[newUnit]) {
+ return {
+ value: Number(
+ ((parsedInterval.value * units[parsedInterval.unit!]) / units[newUnit]).toFixed(2)
+ ),
+ unit: newUnit,
+ };
}
-
- return { value, unit };
};
-export const getSuitableUnit = (intervalInSeconds) =>
+export const getSuitableUnit = (intervalInSeconds: string | number) =>
sortedUnits.find((key, index, array) => {
- const nextUnit = array[index + 1];
+ const nextUnit = array[index + 1] as Unit;
const isValidInput = isNumber(intervalInSeconds) && intervalInSeconds > 0;
const isLastItem = index + 1 === array.length;
return (
isValidInput &&
- ((intervalInSeconds >= units[key] && intervalInSeconds < units[nextUnit]) || isLastItem)
+ ((intervalInSeconds >= units[key as Unit] && intervalInSeconds < units[nextUnit]) ||
+ isLastItem)
);
- });
+ }) as Unit;
-export const getUnitValue = (unit) => units[unit];
+export const getUnitValue = (unit: Unit) => units[unit];
diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts
index cae9058071b6c..75c889af3d5c9 100644
--- a/src/plugins/visualizations/public/vis.ts
+++ b/src/plugins/visualizations/public/vis.ts
@@ -97,13 +97,13 @@ export class Vis {
public readonly uiState: PersistedState;
constructor(visType: string, visState: SerializedVis = {} as any) {
- this.type = this.getType(visType);
+ this.type = this.getType(visType);
this.params = this.getParams(visState.params);
this.uiState = new PersistedState(visState.uiState);
this.id = visState.id;
}
- private getType(visType: string) {
+ private getType(visType: string) {
const type = getTypes().get(visType);
if (!type) {
const errorMessage = i18n.translate('visualizations.visualizationTypeInvalidMessage', {
diff --git a/test/functional/fixtures/es_archiver/discover/data.json b/test/functional/fixtures/es_archiver/discover/data.json
index 0f9820a6c2f6e..0f2edc8c510c3 100644
--- a/test/functional/fixtures/es_archiver/discover/data.json
+++ b/test/functional/fixtures/es_archiver/discover/data.json
@@ -8,7 +8,7 @@
"fields": "[{\"name\":\"@message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"esTypes\":[\"double\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nestedField.child\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"nested\":{\"path\":\"nestedField\"}}},{\"name\":\"phpmemory\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"xss\"}}}]",
"timeFieldName": "@timestamp",
"title": "logstash-*",
- "fieldAttrs": "{\"referer\":{\"customName\":\"Referer custom\"}}"
+ "fieldAttrs": "{\"referer\":{\"customLabel\":\"Referer custom\"}}"
},
"type": "index-pattern"
}
diff --git a/test/functional/fixtures/es_archiver/visualize/data.json b/test/functional/fixtures/es_archiver/visualize/data.json
index c57cdb40ae952..56397351562de 100644
--- a/test/functional/fixtures/es_archiver/visualize/data.json
+++ b/test/functional/fixtures/es_archiver/visualize/data.json
@@ -9,7 +9,7 @@
"fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]",
"timeFieldName": "@timestamp",
"title": "logstash-*",
- "fieldAttrs": "{\"utc_time\":{\"customName\":\"UTC time\"}}"
+ "fieldAttrs": "{\"utc_time\":{\"customLabel\":\"UTC time\"}}"
},
"type": "index-pattern"
}
diff --git a/test/functional/services/listing_table.ts b/test/functional/services/listing_table.ts
index 53b45697136ed..c9f2b8369783c 100644
--- a/test/functional/services/listing_table.ts
+++ b/test/functional/services/listing_table.ts
@@ -62,6 +62,20 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider
return visualizationNames;
}
+ public async waitUntilTableIsLoaded() {
+ return retry.try(async () => {
+ const isLoaded = await find.existsByDisplayedByCssSelector(
+ '[data-test-subj="itemsInMemTable"]:not(.euiBasicTable-loading)'
+ );
+
+ if (isLoaded) {
+ return true;
+ } else {
+ throw new Error('Waiting');
+ }
+ });
+ }
+
/**
* Navigates through all pages on Landing page and returns array of items names
*/
diff --git a/test/tsconfig.json b/test/tsconfig.json
index 2949a764d4b1a..df26441b0806f 100644
--- a/test/tsconfig.json
+++ b/test/tsconfig.json
@@ -1,10 +1,10 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
- "tsBuildInfoFile": "../build/tsbuildinfo/test",
+ "incremental": false,
"types": ["node", "mocha", "flot"]
},
- "include": ["**/*", "../typings/elastic__node_crypto.d.ts", "typings/**/*"],
+ "include": ["**/*", "../typings/elastic__node_crypto.d.ts", "typings/**/*", "../packages/kbn-test/types/ftr_globals/**/*"],
"exclude": ["plugin_functional/plugins/**/*", "interpreter_functional/plugins/**/*"],
"references": [
{ "path": "../src/core/tsconfig.json" },
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 0aad8d6b9c124..111c9dbc949de 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -53,7 +53,6 @@
"types": [
"node",
"jest",
- "react",
"flot",
"jest-styled-components",
"@testing-library/jest-dom"
diff --git a/tsconfig.json b/tsconfig.json
index 00b33bd0b4451..6e137e445762d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,7 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
- "tsBuildInfoFile": "./build/tsbuildinfo/kibana"
+ "incremental": false
},
"include": ["kibana.d.ts", "src/**/*", "typings/**/*", "test_utils/**/*"],
"exclude": [
@@ -31,6 +31,7 @@
{ "path": "./src/core/tsconfig.json" },
{ "path": "./src/plugins/dev_tools/tsconfig.json" },
{ "path": "./src/plugins/inspector/tsconfig.json" },
+ { "path": "./src/plugins/kibana_legacy/tsconfig.json" },
{ "path": "./src/plugins/kibana_react/tsconfig.json" },
{ "path": "./src/plugins/kibana_usage_collection/tsconfig.json" },
{ "path": "./src/plugins/kibana_utils/tsconfig.json" },
@@ -39,6 +40,7 @@
{ "path": "./src/plugins/share/tsconfig.json" },
{ "path": "./src/plugins/telemetry/tsconfig.json" },
{ "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" },
+ { "path": "./src/plugins/url_forwarding/tsconfig.json" },
{ "path": "./src/plugins/usage_collection/tsconfig.json" },
{ "path": "./src/test_utils/tsconfig.json" }
]
diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy
index eea3ff18f3453..0051293704717 100644
--- a/vars/kibanaPipeline.groovy
+++ b/vars/kibanaPipeline.groovy
@@ -390,12 +390,7 @@ def scriptTaskDocker(description, script) {
def buildDocker() {
sh(
- script: """
- cp /usr/local/bin/runbld .ci/
- cp /usr/local/bin/bash_standard_lib.sh .ci/
- cd .ci
- docker build -t kibana-ci -f ./Dockerfile .
- """,
+ script: "./.ci/build_docker.sh",
label: 'Build CI Docker image'
)
}
diff --git a/x-pack/examples/alerting_example/common/constants.ts b/x-pack/examples/alerting_example/common/constants.ts
index dd9cc21954e61..40cc298db795a 100644
--- a/x-pack/examples/alerting_example/common/constants.ts
+++ b/x-pack/examples/alerting_example/common/constants.ts
@@ -8,6 +8,15 @@ export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample';
// always firing
export const DEFAULT_INSTANCES_TO_GENERATE = 5;
+export interface AlwaysFiringParams {
+ instances?: number;
+ thresholds?: {
+ small?: number;
+ medium?: number;
+ large?: number;
+ };
+}
+export type AlwaysFiringActionGroupIds = keyof AlwaysFiringParams['thresholds'];
// Astros
export enum Craft {
diff --git a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx
index a5d158fca836b..abbe1d2a48d11 100644
--- a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx
+++ b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx
@@ -4,17 +4,31 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiFieldNumber, EuiFormRow } from '@elastic/eui';
+import React, { Fragment, useState } from 'react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFieldNumber,
+ EuiFormRow,
+ EuiPopover,
+ EuiExpression,
+ EuiSpacer,
+} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { AlertTypeModel } from '../../../../plugins/triggers_actions_ui/public';
-import { DEFAULT_INSTANCES_TO_GENERATE } from '../../common/constants';
-
-interface AlwaysFiringParamsProps {
- alertParams: { instances?: number };
- setAlertParams: (property: string, value: any) => void;
- errors: { [key: string]: string[] };
-}
+import { omit, pick } from 'lodash';
+import {
+ ActionGroupWithCondition,
+ AlertConditions,
+ AlertConditionsGroup,
+ AlertTypeModel,
+ AlertTypeParamsExpressionProps,
+ AlertsContextValue,
+} from '../../../../plugins/triggers_actions_ui/public';
+import {
+ AlwaysFiringParams,
+ AlwaysFiringActionGroupIds,
+ DEFAULT_INSTANCES_TO_GENERATE,
+} from '../../common/constants';
export function getAlertType(): AlertTypeModel {
return {
@@ -24,7 +38,7 @@ export function getAlertType(): AlertTypeModel {
iconClass: 'bolt',
documentationUrl: null,
alertParamsExpression: AlwaysFiringExpression,
- validate: (alertParams: AlwaysFiringParamsProps['alertParams']) => {
+ validate: (alertParams: AlwaysFiringParams) => {
const { instances } = alertParams;
const validationResult = {
errors: {
@@ -44,11 +58,30 @@ export function getAlertType(): AlertTypeModel {
};
}
-export const AlwaysFiringExpression: React.FunctionComponent = ({
- alertParams,
- setAlertParams,
-}) => {
- const { instances = DEFAULT_INSTANCES_TO_GENERATE } = alertParams;
+const DEFAULT_THRESHOLDS: AlwaysFiringParams['thresholds'] = {
+ small: 0,
+ medium: 5000,
+ large: 10000,
+};
+
+export const AlwaysFiringExpression: React.FunctionComponent> = ({ alertParams, setAlertParams, actionGroups, defaultActionGroupId }) => {
+ const {
+ instances = DEFAULT_INSTANCES_TO_GENERATE,
+ thresholds = pick(DEFAULT_THRESHOLDS, defaultActionGroupId),
+ } = alertParams;
+
+ const actionGroupsWithConditions = actionGroups.map((actionGroup) =>
+ Number.isInteger(thresholds[actionGroup.id as AlwaysFiringActionGroupIds])
+ ? {
+ ...actionGroup,
+ conditions: thresholds[actionGroup.id as AlwaysFiringActionGroupIds]!,
+ }
+ : actionGroup
+ );
+
return (
@@ -67,6 +100,88 @@ export const AlwaysFiringExpression: React.FunctionComponent
+
+
+
+ {
+ setAlertParams('thresholds', {
+ ...thresholds,
+ ...pick(DEFAULT_THRESHOLDS, actionGroup.id),
+ });
+ }}
+ >
+ {
+ setAlertParams('thresholds', omit(thresholds, actionGroup.id));
+ }}
+ >
+ {
+ setAlertParams('thresholds', {
+ ...thresholds,
+ [actionGroup.id]: actionGroup.conditions,
+ });
+ }}
+ />
+
+
+
+
+
);
};
+
+interface TShirtSelectorProps {
+ actionGroup?: ActionGroupWithCondition;
+ setTShirtThreshold: (actionGroup: ActionGroupWithCondition) => void;
+}
+const TShirtSelector = ({ actionGroup, setTShirtThreshold }: TShirtSelectorProps) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ if (!actionGroup) {
+ return null;
+ }
+
+ return (
+ setIsOpen(true)}
+ />
+ }
+ isOpen={isOpen}
+ closePopover={() => setIsOpen(false)}
+ ownFocus
+ anchorPosition="downLeft"
+ >
+
+
+ {'Is Above'}
+
+
+ {
+ const conditions = parseInt(e.target.value, 10);
+ if (e.target.value && !isNaN(conditions)) {
+ setTShirtThreshold({
+ ...actionGroup,
+ conditions,
+ });
+ }
+ }}
+ />
+
+
+
+ );
+};
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 d02406a23045e..1900f55a51a55 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,31 +5,56 @@
*/
import uuid from 'uuid';
-import { range, random } from 'lodash';
+import { range } from 'lodash';
import { AlertType } from '../../../../plugins/alerts/server';
-import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
+import {
+ DEFAULT_INSTANCES_TO_GENERATE,
+ ALERTING_EXAMPLE_APP_ID,
+ AlwaysFiringParams,
+} from '../../common/constants';
const ACTION_GROUPS = [
- { id: 'small', name: 'small' },
- { id: 'medium', name: 'medium' },
- { id: 'large', name: 'large' },
+ { id: 'small', name: 'Small t-shirt' },
+ { id: 'medium', name: 'Medium t-shirt' },
+ { id: 'large', name: 'Large t-shirt' },
];
+const DEFAULT_ACTION_GROUP = 'small';
-export const alertType: AlertType = {
+function getTShirtSizeByIdAndThreshold(id: string, thresholds: AlwaysFiringParams['thresholds']) {
+ const idAsNumber = parseInt(id, 10);
+ if (!isNaN(idAsNumber)) {
+ if (thresholds?.large && thresholds.large < idAsNumber) {
+ return 'large';
+ }
+ if (thresholds?.medium && thresholds.medium < idAsNumber) {
+ return 'medium';
+ }
+ if (thresholds?.small && thresholds.small < idAsNumber) {
+ return 'small';
+ }
+ }
+ return DEFAULT_ACTION_GROUP;
+}
+
+export const alertType: AlertType = {
id: 'example.always-firing',
name: 'Always firing',
actionGroups: ACTION_GROUPS,
- defaultActionGroupId: 'small',
- async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) {
+ defaultActionGroupId: DEFAULT_ACTION_GROUP,
+ async executor({
+ services,
+ params: { instances = DEFAULT_INSTANCES_TO_GENERATE, thresholds },
+ state,
+ }) {
const count = (state.count ?? 0) + 1;
range(instances)
- .map(() => ({ id: uuid.v4(), tshirtSize: ACTION_GROUPS[random(0, 2)].id! }))
- .forEach((instance: { id: string; tshirtSize: string }) => {
+ .map(() => uuid.v4())
+ .forEach((id: string) => {
services
- .alertInstanceFactory(instance.id)
+ .alertInstanceFactory(id)
.replaceState({ triggerdOnCycle: count })
- .scheduleActions(instance.tshirtSize);
+ .scheduleActions(getTShirtSizeByIdAndThreshold(id, thresholds));
});
return {
diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts
index ed06bd888f919..8adbedf069d30 100644
--- a/x-pack/plugins/actions/server/create_execute_function.test.ts
+++ b/x-pack/plugins/actions/server/create_execute_function.test.ts
@@ -172,7 +172,7 @@ describe('execute()', () => {
apiKey: null,
})
).rejects.toThrowErrorMatchingInlineSnapshot(
- `"Unable to execute action due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"`
+ `"Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."`
);
});
diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts
index f0a22c642cf61..dc400cb90967a 100644
--- a/x-pack/plugins/actions/server/create_execute_function.ts
+++ b/x-pack/plugins/actions/server/create_execute_function.ts
@@ -41,7 +41,7 @@ export function createExecutionEnqueuerFunction({
) {
if (isESOUsingEphemeralEncryptionKey === true) {
throw new Error(
- `Unable to execute action due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml`
+ `Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.`
);
}
diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts
index 4ff56536e3867..57b88d3e6c1d8 100644
--- a/x-pack/plugins/actions/server/lib/action_executor.test.ts
+++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts
@@ -31,7 +31,7 @@ const executeParams = {
request: {} as KibanaRequest,
};
-const spacesMock = spacesServiceMock.createSetupContract();
+const spacesMock = spacesServiceMock.createStartContract();
const loggerMock = loggingSystemMock.create().get();
const getActionsClientWithRequest = jest.fn();
actionExecutor.initialize({
@@ -322,7 +322,7 @@ test('throws an error when passing isESOUsingEphemeralEncryptionKey with value o
await expect(
customActionExecutor.execute(executeParams)
).rejects.toThrowErrorMatchingInlineSnapshot(
- `"Unable to execute action due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"`
+ `"Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."`
);
});
diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts
index af70fbf2ec896..d050bab9b0d9f 100644
--- a/x-pack/plugins/actions/server/lib/action_executor.ts
+++ b/x-pack/plugins/actions/server/lib/action_executor.ts
@@ -15,7 +15,7 @@ import {
ProxySettings,
} from '../types';
import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server';
-import { SpacesServiceSetup } from '../../../spaces/server';
+import { SpacesServiceStart } from '../../../spaces/server';
import { EVENT_LOG_ACTIONS } from '../plugin';
import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
import { ActionsClient } from '../actions_client';
@@ -23,7 +23,7 @@ import { ActionExecutionSource } from './action_execution_source';
export interface ActionExecutorContext {
logger: Logger;
- spaces?: SpacesServiceSetup;
+ spaces?: SpacesServiceStart;
getServices: GetServicesFunction;
getActionsClientWithRequest: (
request: KibanaRequest,
@@ -74,7 +74,7 @@ export class ActionExecutor {
if (this.isESOUsingEphemeralEncryptionKey === true) {
throw new Error(
- `Unable to execute action due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml`
+ `Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.`
);
}
diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
index 18cbd9f9c5fad..136ca5cb98465 100644
--- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
+++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
@@ -12,7 +12,7 @@ import { TaskRunnerFactory } from './task_runner_factory';
import { actionTypeRegistryMock } from '../action_type_registry.mock';
import { actionExecutorMock } from './action_executor.mock';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks';
-import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks';
+import { savedObjectsClientMock, loggingSystemMock, httpServiceMock } from 'src/core/server/mocks';
import { eventLoggerMock } from '../../../event_log/server/mocks';
import { ActionTypeDisabledError } from './errors';
import { actionsClientMock } from '../mocks';
@@ -70,7 +70,7 @@ const taskRunnerFactoryInitializerParams = {
actionTypeRegistry,
logger: loggingSystemMock.create().get(),
encryptedSavedObjectsClient: mockedEncryptedSavedObjectsClient,
- getBasePath: jest.fn().mockReturnValue(undefined),
+ basePathService: httpServiceMock.createBasePath(),
getUnsecuredSavedObjectsClient: jest.fn().mockReturnValue(services.savedObjectsClient),
};
@@ -126,27 +126,23 @@ test('executes the task by calling the executor with proper parameters', async (
expect(
mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser
).toHaveBeenCalledWith('action_task_params', '3', { namespace: 'namespace-test' });
+
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
actionId: '2',
params: { baz: true },
- request: {
- getBasePath: expect.any(Function),
+ request: expect.objectContaining({
headers: {
// base64 encoded "123:abc"
authorization: 'ApiKey MTIzOmFiYw==',
},
- path: '/',
- route: { settings: {} },
- url: {
- href: '/',
- },
- raw: {
- req: {
- url: '/',
- },
- },
- },
+ }),
});
+
+ const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
+ expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
+ executeParams.request,
+ '/s/test'
+ );
});
test('cleans up action_task_params object', async () => {
@@ -255,24 +251,19 @@ test('uses API key when provided', async () => {
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
actionId: '2',
params: { baz: true },
- request: {
- getBasePath: expect.anything(),
+ request: expect.objectContaining({
headers: {
// base64 encoded "123:abc"
authorization: 'ApiKey MTIzOmFiYw==',
},
- path: '/',
- route: { settings: {} },
- url: {
- href: '/',
- },
- raw: {
- req: {
- url: '/',
- },
- },
- },
+ }),
});
+
+ const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
+ expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
+ executeParams.request,
+ '/s/test'
+ );
});
test(`doesn't use API key when not provided`, async () => {
@@ -297,21 +288,16 @@ test(`doesn't use API key when not provided`, async () => {
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
actionId: '2',
params: { baz: true },
- request: {
- getBasePath: expect.anything(),
+ request: expect.objectContaining({
headers: {},
- path: '/',
- route: { settings: {} },
- url: {
- href: '/',
- },
- raw: {
- req: {
- url: '/',
- },
- },
- },
+ }),
});
+
+ const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
+ expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
+ executeParams.request,
+ '/s/test'
+ );
});
test(`throws an error when license doesn't support the action type`, async () => {
diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts
index aeeeb4ed7d520..99c8b8b1ff0e1 100644
--- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts
+++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts
@@ -5,14 +5,17 @@
*/
import { pick } from 'lodash';
+import type { Request } from '@hapi/hapi';
import { pipe } from 'fp-ts/lib/pipeable';
import { map, fromNullable, getOrElse } from 'fp-ts/lib/Option';
+import { addSpaceIdToPath } from '../../../spaces/server';
import {
Logger,
SavedObjectsClientContract,
KibanaRequest,
SavedObjectReference,
-} from 'src/core/server';
+ IBasePath,
+} from '../../../../../src/core/server';
import { ActionExecutorContract } from './action_executor';
import { ExecutorError } from './executor_error';
import { RunContext } from '../../../task_manager/server';
@@ -21,7 +24,6 @@ import { ActionTypeDisabledError } from './errors';
import {
ActionTaskParams,
ActionTypeRegistryContract,
- GetBasePathFunction,
SpaceIdToNamespaceFunction,
ActionTypeExecutorResult,
} from '../types';
@@ -33,7 +35,7 @@ export interface TaskRunnerContext {
actionTypeRegistry: ActionTypeRegistryContract;
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
spaceIdToNamespace: SpaceIdToNamespaceFunction;
- getBasePath: GetBasePathFunction;
+ basePathService: IBasePath;
getUnsecuredSavedObjectsClient: (request: KibanaRequest) => SavedObjectsClientContract;
}
@@ -64,7 +66,7 @@ export class TaskRunnerFactory {
logger,
encryptedSavedObjectsClient,
spaceIdToNamespace,
- getBasePath,
+ basePathService,
getUnsecuredSavedObjectsClient,
} = this.taskRunnerContext!;
@@ -87,11 +89,12 @@ export class TaskRunnerFactory {
requestHeaders.authorization = `ApiKey ${apiKey}`;
}
+ const path = addSpaceIdToPath('/', spaceId);
+
// Since we're using API keys and accessing elasticsearch can only be done
// via a request, we're faking one with the proper authorization headers.
- const fakeRequest = ({
+ const fakeRequest = KibanaRequest.from(({
headers: requestHeaders,
- getBasePath: () => getBasePath(spaceId),
path: '/',
route: { settings: {} },
url: {
@@ -102,7 +105,9 @@ export class TaskRunnerFactory {
url: '/',
},
},
- } as unknown) as KibanaRequest;
+ } as unknown) as Request);
+
+ basePathService.set(fakeRequest, path);
let executorResult: ActionTypeExecutorResult;
try {
diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts
index 7f7f9e196da07..ff43b05b6d895 100644
--- a/x-pack/plugins/actions/server/plugin.test.ts
+++ b/x-pack/plugins/actions/server/plugin.test.ts
@@ -56,7 +56,7 @@ describe('Actions Plugin', () => {
await plugin.setup(coreSetup as any, pluginsSetup);
expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true);
expect(context.logger.get().warn).toHaveBeenCalledWith(
- 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.'
+ 'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
);
});
@@ -116,7 +116,7 @@ describe('Actions Plugin', () => {
httpServerMock.createResponseFactory()
)) as unknown) as RequestHandlerContext['actions'];
expect(() => actionsContextHandler!.getActionsClient()).toThrowErrorMatchingInlineSnapshot(
- `"Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"`
+ `"Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."`
);
});
});
@@ -252,7 +252,7 @@ describe('Actions Plugin', () => {
await expect(
pluginStart.getActionsClientWithRequest(httpServerMock.createKibanaRequest())
).rejects.toThrowErrorMatchingInlineSnapshot(
- `"Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"`
+ `"Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."`
);
});
});
diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts
index 9db07f653872f..a160735e89a93 100644
--- a/x-pack/plugins/actions/server/plugin.ts
+++ b/x-pack/plugins/actions/server/plugin.ts
@@ -27,7 +27,7 @@ import {
} from '../../encrypted_saved_objects/server';
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server';
-import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server';
+import { SpacesPluginStart } from '../../spaces/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { SecurityPluginSetup } from '../../security/server';
@@ -109,7 +109,6 @@ export interface ActionsPluginsSetup {
taskManager: TaskManagerSetupContract;
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup;
licensing: LicensingPluginSetup;
- spaces?: SpacesPluginSetup;
eventLog: IEventLogService;
usageCollection?: UsageCollectionSetup;
security?: SecurityPluginSetup;
@@ -119,6 +118,7 @@ export interface ActionsPluginsStart {
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
taskManager: TaskManagerStartContract;
licensing: LicensingPluginStart;
+ spaces?: SpacesPluginStart;
}
const includedHiddenTypes = [
@@ -133,12 +133,10 @@ export class ActionsPlugin implements Plugin, Plugi
private readonly logger: Logger;
private actionsConfig?: ActionsConfig;
- private serverBasePath?: string;
private taskRunnerFactory?: TaskRunnerFactory;
private actionTypeRegistry?: ActionTypeRegistry;
private actionExecutor?: ActionExecutor;
private licenseState: ILicenseState | null = null;
- private spaces?: SpacesServiceSetup;
private security?: SecurityPluginSetup;
private eventLogService?: IEventLogService;
private eventLogger?: IEventLogger;
@@ -171,7 +169,7 @@ export class ActionsPlugin implements Plugin, Plugi
if (this.isESOUsingEphemeralEncryptionKey) {
this.logger.warn(
- 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.'
+ 'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
);
}
@@ -211,9 +209,7 @@ export class ActionsPlugin implements Plugin, Plugi
});
this.taskRunnerFactory = taskRunnerFactory;
this.actionTypeRegistry = actionTypeRegistry;
- this.serverBasePath = core.http.basePath.serverBasePath;
this.actionExecutor = actionExecutor;
- this.spaces = plugins.spaces?.spacesService;
this.security = plugins.security;
registerBuiltInActionTypes({
@@ -292,7 +288,7 @@ export class ActionsPlugin implements Plugin, Plugi
) => {
if (isESOUsingEphemeralEncryptionKey === true) {
throw new Error(
- `Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml`
+ `Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.`
);
}
@@ -339,7 +335,7 @@ export class ActionsPlugin implements Plugin, Plugi
actionExecutor!.initialize({
logger,
eventLogger: this.eventLogger!,
- spaces: this.spaces,
+ spaces: plugins.spaces?.spacesService,
getActionsClientWithRequest,
getServices: this.getServicesFactory(
getScopedSavedObjectsClientWithoutAccessToActions,
@@ -359,12 +355,18 @@ export class ActionsPlugin implements Plugin, Plugi
: undefined,
});
+ const spaceIdToNamespace = (spaceId?: string) => {
+ return plugins.spaces && spaceId
+ ? plugins.spaces.spacesService.spaceIdToNamespace(spaceId)
+ : undefined;
+ };
+
taskRunnerFactory!.initialize({
logger,
actionTypeRegistry: actionTypeRegistry!,
encryptedSavedObjectsClient,
- getBasePath: this.getBasePath,
- spaceIdToNamespace: this.spaceIdToNamespace,
+ basePathService: core.http.basePath,
+ spaceIdToNamespace,
getUnsecuredSavedObjectsClient: (request: KibanaRequest) =>
this.getUnsecuredSavedObjectsClient(core.savedObjects, request),
});
@@ -446,7 +448,7 @@ export class ActionsPlugin implements Plugin, Plugi
getActionsClient: () => {
if (isESOUsingEphemeralEncryptionKey === true) {
throw new Error(
- `Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml`
+ `Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.`
);
}
return new ActionsClient({
@@ -474,14 +476,6 @@ export class ActionsPlugin implements Plugin, Plugi
};
};
- private spaceIdToNamespace = (spaceId?: string): string | undefined => {
- return this.spaces && spaceId ? this.spaces.spaceIdToNamespace(spaceId) : undefined;
- };
-
- private getBasePath = (spaceId?: string): string => {
- return this.spaces && spaceId ? this.spaces.getBasePath(spaceId) : this.serverBasePath!;
- };
-
public stop() {
if (this.licenseState) {
this.licenseState.clean();
diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts
index 1867815bd5f90..79895195d90f3 100644
--- a/x-pack/plugins/actions/server/types.ts
+++ b/x-pack/plugins/actions/server/types.ts
@@ -22,7 +22,6 @@ export { ActionTypeExecutorResult } from '../common';
export type WithoutQueryAndParams = Pick>;
export type GetServicesFunction = (request: KibanaRequest) => Services;
export type ActionTypeRegistryContract = PublicMethodsOf;
-export type GetBasePathFunction = (spaceId?: string) => string;
export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined;
export type ActionTypeConfig = Record;
export type ActionTypeSecrets = Record;
diff --git a/x-pack/plugins/alerts/common/alert.ts b/x-pack/plugins/alerts/common/alert.ts
index 97a9a58400e38..88f6090d20737 100644
--- a/x-pack/plugins/alerts/common/alert.ts
+++ b/x-pack/plugins/alerts/common/alert.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SavedObjectAttributes } from 'kibana/server';
+import { SavedObjectAttribute, SavedObjectAttributes } from 'kibana/server';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AlertTypeState = Record;
@@ -37,6 +37,7 @@ export interface AlertExecutionStatus {
}
export type AlertActionParams = SavedObjectAttributes;
+export type AlertActionParam = SavedObjectAttribute;
export interface AlertAction {
group: string;
diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts
index e97b37f16faf0..c08ff9449d151 100644
--- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts
@@ -228,14 +228,17 @@ export class AlertsClient {
this.validateActions(alertType, data.actions);
+ const createTime = Date.now();
const { references, actions } = await this.denormalizeActions(data.actions);
+
const rawAlert: RawAlert = {
...data,
...this.apiKeyAsAlertAttributes(createdAPIKey, username),
actions,
createdBy: username,
updatedBy: username,
- createdAt: new Date().toISOString(),
+ createdAt: new Date(createTime).toISOString(),
+ updatedAt: new Date(createTime).toISOString(),
params: validatedAlertTypeParams as RawAlert['params'],
muteAll: false,
mutedInstanceIds: [],
@@ -289,12 +292,7 @@ export class AlertsClient {
});
createdAlert.attributes.scheduledTaskId = scheduledTask.id;
}
- return this.getAlertFromRaw(
- createdAlert.id,
- createdAlert.attributes,
- createdAlert.updated_at,
- references
- );
+ return this.getAlertFromRaw(createdAlert.id, createdAlert.attributes, references);
}
public async get({ id }: { id: string }): Promise {
@@ -304,7 +302,7 @@ export class AlertsClient {
result.attributes.consumer,
ReadOperations.Get
);
- return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references);
+ return this.getAlertFromRaw(result.id, result.attributes, result.references);
}
public async getAlertState({ id }: { id: string }): Promise {
@@ -393,13 +391,11 @@ export class AlertsClient {
type: 'alert',
});
- // eslint-disable-next-line @typescript-eslint/naming-convention
- const authorizedData = data.map(({ id, attributes, updated_at, references }) => {
+ const authorizedData = data.map(({ id, attributes, references }) => {
ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer);
return this.getAlertFromRaw(
id,
fields ? (pick(attributes, fields) as RawAlert) : attributes,
- updated_at,
references
);
});
@@ -585,6 +581,7 @@ export class AlertsClient {
params: validatedAlertTypeParams as RawAlert['params'],
actions,
updatedBy: username,
+ updatedAt: new Date().toISOString(),
});
try {
updatedObject = await this.unsecuredSavedObjectsClient.create(
@@ -607,12 +604,7 @@ export class AlertsClient {
throw e;
}
- return this.getPartialAlertFromRaw(
- id,
- updatedObject.attributes,
- updatedObject.updated_at,
- updatedObject.references
- );
+ return this.getPartialAlertFromRaw(id, updatedObject.attributes, updatedObject.references);
}
private apiKeyAsAlertAttributes(
@@ -677,6 +669,7 @@ export class AlertsClient {
await this.createAPIKey(this.generateAPIKeyName(attributes.alertTypeId, attributes.name)),
username
),
+ updatedAt: new Date().toISOString(),
updatedBy: username,
});
try {
@@ -751,6 +744,7 @@ export class AlertsClient {
username
),
updatedBy: username,
+ updatedAt: new Date().toISOString(),
});
try {
await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version });
@@ -829,6 +823,7 @@ export class AlertsClient {
apiKey: null,
apiKeyOwner: null,
updatedBy: await this.getUserName(),
+ updatedAt: new Date().toISOString(),
}),
{ version }
);
@@ -875,6 +870,7 @@ export class AlertsClient {
muteAll: true,
mutedInstanceIds: [],
updatedBy: await this.getUserName(),
+ updatedAt: new Date().toISOString(),
});
const updateOptions = { version };
@@ -913,6 +909,7 @@ export class AlertsClient {
muteAll: false,
mutedInstanceIds: [],
updatedBy: await this.getUserName(),
+ updatedAt: new Date().toISOString(),
});
const updateOptions = { version };
@@ -957,6 +954,7 @@ export class AlertsClient {
this.updateMeta({
mutedInstanceIds,
updatedBy: await this.getUserName(),
+ updatedAt: new Date().toISOString(),
}),
{ version }
);
@@ -999,6 +997,7 @@ export class AlertsClient {
alertId,
this.updateMeta({
updatedBy: await this.getUserName(),
+ updatedAt: new Date().toISOString(),
mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId),
}),
{ version }
@@ -1050,19 +1049,17 @@ export class AlertsClient {
private getAlertFromRaw(
id: string,
rawAlert: RawAlert,
- updatedAt: SavedObject['updated_at'],
references: SavedObjectReference[] | undefined
): Alert {
// In order to support the partial update API of Saved Objects we have to support
// partial updates of an Alert, but when we receive an actual RawAlert, it is safe
// to cast the result to an Alert
- return this.getPartialAlertFromRaw(id, rawAlert, updatedAt, references) as Alert;
+ return this.getPartialAlertFromRaw(id, rawAlert, references) as Alert;
}
private getPartialAlertFromRaw(
id: string,
- { createdAt, meta, scheduledTaskId, ...rawAlert }: Partial,
- updatedAt: SavedObject['updated_at'] = createdAt,
+ { createdAt, updatedAt, meta, scheduledTaskId, ...rawAlert }: Partial,
references: SavedObjectReference[] | undefined
): PartialAlert {
// Not the prettiest code here, but if we want to use most of the
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts
index ee407b1a6d50c..6d259029ac480 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts
@@ -196,6 +196,7 @@ describe('create()', () => {
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
updatedBy: 'elastic',
+ updatedAt: '2019-02-12T21:01:22.479Z',
muteAll: false,
mutedInstanceIds: [],
actions: [
@@ -330,6 +331,7 @@ describe('create()', () => {
"foo",
],
"throttle": null,
+ "updatedAt": "2019-02-12T21:01:22.479Z",
"updatedBy": "elastic",
}
`);
@@ -418,6 +420,7 @@ describe('create()', () => {
bar: true,
},
createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',
@@ -555,6 +558,7 @@ describe('create()', () => {
bar: true,
},
createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',
@@ -631,6 +635,7 @@ describe('create()', () => {
bar: true,
},
createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',
@@ -971,6 +976,7 @@ describe('create()', () => {
createdBy: 'elastic',
createdAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
+ updatedAt: '2019-02-12T21:01:22.479Z',
enabled: true,
meta: {
versionApiKeyLastmodified: 'v7.10.0',
@@ -1092,6 +1098,7 @@ describe('create()', () => {
createdBy: 'elastic',
createdAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
+ updatedAt: '2019-02-12T21:01:22.479Z',
enabled: false,
meta: {
versionApiKeyLastmodified: 'v7.10.0',
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts
index 11ce0027f82d8..8c9ab9494a50a 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts
@@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
-import { getBeforeSetup } from './lib';
+import { getBeforeSetup, setGlobalDate } from './lib';
import { InvalidatePendingApiKey } from '../../types';
const taskManager = taskManagerMock.createStart();
@@ -45,6 +45,8 @@ beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
});
+setGlobalDate();
+
describe('disable()', () => {
let alertsClient: AlertsClient;
const existingAlert = {
@@ -136,6 +138,7 @@ describe('disable()', () => {
scheduledTaskId: null,
apiKey: null,
apiKeyOwner: null,
+ updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
actions: [
{
@@ -190,6 +193,7 @@ describe('disable()', () => {
scheduledTaskId: null,
apiKey: null,
apiKeyOwner: null,
+ updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
actions: [
{
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts
index 16e83c42d8930..feec1d1b9334a 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts
@@ -13,7 +13,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { TaskStatus } from '../../../../task_manager/server';
-import { getBeforeSetup } from './lib';
+import { getBeforeSetup, setGlobalDate } from './lib';
import { InvalidatePendingApiKey } from '../../types';
const taskManager = taskManagerMock.createStart();
@@ -46,6 +46,8 @@ beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
});
+setGlobalDate();
+
describe('enable()', () => {
let alertsClient: AlertsClient;
const existingAlert = {
@@ -186,6 +188,7 @@ describe('enable()', () => {
meta: {
versionApiKeyLastmodified: kibanaVersion,
},
+ updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
apiKey: null,
apiKeyOwner: null,
@@ -292,6 +295,7 @@ describe('enable()', () => {
apiKey: Buffer.from('123:abc').toString('base64'),
apiKeyOwner: 'elastic',
updatedBy: 'elastic',
+ updatedAt: '2019-02-12T21:01:22.479Z',
actions: [
{
group: 'default',
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts
index 1b3a776bd23e0..3d7473a746986 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts
@@ -79,6 +79,7 @@ describe('find()', () => {
bar: true,
},
createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts
index 5c0d80f159b31..3f0c783f424d1 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts
@@ -59,6 +59,7 @@ describe('get()', () => {
bar: true,
},
createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts
index 269b2eb2ab7a7..9bd61c0fe66d2 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts
@@ -76,6 +76,7 @@ const BaseAlertInstanceSummarySavedObject: SavedObject = {
createdBy: null,
updatedBy: null,
createdAt: mockedDateString,
+ updatedAt: mockedDateString,
apiKey: null,
apiKeyOwner: null,
throttle: null,
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts
index 868fa3d8c6aa2..14ebca2135587 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts
@@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
-import { getBeforeSetup } from './lib';
+import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
@@ -43,6 +43,8 @@ beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
});
+setGlobalDate();
+
describe('muteAll()', () => {
test('mutes an alert', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
@@ -74,6 +76,7 @@ describe('muteAll()', () => {
{
muteAll: true,
mutedInstanceIds: [],
+ updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
},
{
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts
index 05ca741f480ca..c2188f128cb4d 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts
@@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
-import { getBeforeSetup } from './lib';
+import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
@@ -44,6 +44,8 @@ beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
});
+setGlobalDate();
+
describe('muteInstance()', () => {
test('mutes an alert instance', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
@@ -68,6 +70,7 @@ describe('muteInstance()', () => {
'1',
{
mutedInstanceIds: ['2'],
+ updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
},
{
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts
index 5ef1af9b6f0ee..d92304ab873be 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts
@@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
-import { getBeforeSetup } from './lib';
+import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
@@ -44,6 +44,8 @@ beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
});
+setGlobalDate();
+
describe('unmuteAll()', () => {
test('unmutes an alert', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
@@ -75,6 +77,7 @@ describe('unmuteAll()', () => {
{
muteAll: false,
mutedInstanceIds: [],
+ updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
},
{
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts
index 88692239ac2fe..3486df98f2f05 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts
@@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
-import { getBeforeSetup } from './lib';
+import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
@@ -44,6 +44,8 @@ beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
});
+setGlobalDate();
+
describe('unmuteInstance()', () => {
test('unmutes an alert instance', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
@@ -69,6 +71,7 @@ describe('unmuteInstance()', () => {
{
mutedInstanceIds: [],
updatedBy: 'elastic',
+ updatedAt: '2019-02-12T21:01:22.479Z',
},
{ version: '123' }
);
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts
index ad58e36ade722..d0bb2607f7a47 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts
@@ -140,8 +140,8 @@ describe('update()', () => {
],
scheduledTaskId: 'task-123',
createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
},
- updated_at: new Date().toISOString(),
references: [
{
name: 'action_0',
@@ -300,6 +300,7 @@ describe('update()', () => {
"foo",
],
"throttle": null,
+ "updatedAt": "2019-02-12T21:01:22.479Z",
"updatedBy": "elastic",
}
`);
@@ -362,6 +363,7 @@ describe('update()', () => {
bar: true,
},
createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',
@@ -484,6 +486,7 @@ describe('update()', () => {
"foo",
],
"throttle": "5m",
+ "updatedAt": "2019-02-12T21:01:22.479Z",
"updatedBy": "elastic",
}
`);
@@ -534,6 +537,7 @@ describe('update()', () => {
bar: true,
},
createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',
@@ -648,6 +652,7 @@ describe('update()', () => {
"foo",
],
"throttle": "5m",
+ "updatedAt": "2019-02-12T21:01:22.479Z",
"updatedBy": "elastic",
}
`);
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts
index af178a1fac5f5..ca5f44078f513 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts
@@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
-import { getBeforeSetup } from './lib';
+import { getBeforeSetup, setGlobalDate } from './lib';
import { InvalidatePendingApiKey } from '../../types';
const taskManager = taskManagerMock.createStart();
@@ -44,6 +44,8 @@ beforeEach(() => {
getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry);
});
+setGlobalDate();
+
describe('updateApiKey()', () => {
let alertsClient: AlertsClient;
const existingAlert = {
@@ -113,6 +115,7 @@ describe('updateApiKey()', () => {
apiKey: Buffer.from('234:abc').toString('base64'),
apiKeyOwner: 'elastic',
updatedBy: 'elastic',
+ updatedAt: '2019-02-12T21:01:22.479Z',
actions: [
{
group: 'default',
@@ -162,6 +165,7 @@ describe('updateApiKey()', () => {
enabled: true,
apiKey: Buffer.from('234:abc').toString('base64'),
apiKeyOwner: 'elastic',
+ updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
actions: [
{
diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts
index 62f4b7d5a3fc4..fee7901c4ea55 100644
--- a/x-pack/plugins/alerts/server/plugin.test.ts
+++ b/x-pack/plugins/alerts/server/plugin.test.ts
@@ -52,7 +52,7 @@ describe('Alerting Plugin', () => {
expect(statusMock.set).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsSetup.usingEphemeralEncryptionKey).toEqual(true);
expect(context.logger.get().warn).toHaveBeenCalledWith(
- 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.'
+ 'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
);
});
});
@@ -113,7 +113,7 @@ describe('Alerting Plugin', () => {
expect(() =>
startContract.getAlertsClientWithRequest({} as KibanaRequest)
).toThrowErrorMatchingInlineSnapshot(
- `"Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"`
+ `"Unable to create alerts client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."`
);
});
@@ -158,7 +158,6 @@ describe('Alerting Plugin', () => {
getActionsClientWithRequest: jest.fn(),
getActionsAuthorizationWithRequest: jest.fn(),
},
- spaces: () => null,
encryptedSavedObjects: encryptedSavedObjectsMock.createStart(),
features: mockFeatures(),
} as unknown) as AlertingPluginsStart
diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts
index 0c91e93938346..99cb45130718a 100644
--- a/x-pack/plugins/alerts/server/plugin.ts
+++ b/x-pack/plugins/alerts/server/plugin.ts
@@ -13,7 +13,7 @@ import {
EncryptedSavedObjectsPluginStart,
} from '../../encrypted_saved_objects/server';
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
-import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server';
+import { SpacesPluginStart } from '../../spaces/server';
import { AlertsClient } from './alerts_client';
import { AlertTypeRegistry } from './alert_type_registry';
import { TaskRunnerFactory } from './task_runner';
@@ -101,7 +101,6 @@ export interface AlertingPluginsSetup {
actions: ActionsPluginSetupContract;
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup;
licensing: LicensingPluginSetup;
- spaces?: SpacesPluginSetup;
usageCollection?: UsageCollectionSetup;
eventLog: IEventLogService;
statusService: StatusServiceSetup;
@@ -112,6 +111,7 @@ export interface AlertingPluginsStart {
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
features: FeaturesPluginStart;
eventLog: IEventLogClientService;
+ spaces?: SpacesPluginStart;
}
export class AlertingPlugin {
@@ -119,10 +119,8 @@ export class AlertingPlugin {
private readonly logger: Logger;
private alertTypeRegistry?: AlertTypeRegistry;
private readonly taskRunnerFactory: TaskRunnerFactory;
- private serverBasePath?: string;
private licenseState: LicenseState | null = null;
private isESOUsingEphemeralEncryptionKey?: boolean;
- private spaces?: SpacesServiceSetup;
private security?: SecurityPluginSetup;
private readonly alertsClientFactory: AlertsClientFactory;
private readonly telemetryLogger: Logger;
@@ -151,7 +149,6 @@ export class AlertingPlugin {
plugins: AlertingPluginsSetup
): Promise {
this.licenseState = new LicenseState(plugins.licensing.license$);
- this.spaces = plugins.spaces?.spacesService;
this.security = plugins.security;
core.capabilities.registerProvider(() => {
@@ -169,7 +166,7 @@ export class AlertingPlugin {
if (this.isESOUsingEphemeralEncryptionKey) {
this.logger.warn(
- 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.'
+ 'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
);
}
@@ -188,8 +185,6 @@ export class AlertingPlugin {
});
this.alertTypeRegistry = alertTypeRegistry;
- this.serverBasePath = core.http.basePath.serverBasePath;
-
const usageCollection = plugins.usageCollection;
if (usageCollection) {
initializeAlertingTelemetry(
@@ -261,7 +256,6 @@ export class AlertingPlugin {
public start(core: CoreStart, plugins: AlertingPluginsStart): PluginStartContract {
const {
- spaces,
isESOUsingEphemeralEncryptionKey,
logger,
taskRunnerFactory,
@@ -274,18 +268,24 @@ export class AlertingPlugin {
includedHiddenTypes: ['alert'],
});
+ const spaceIdToNamespace = (spaceId?: string) => {
+ return plugins.spaces && spaceId
+ ? plugins.spaces.spacesService.spaceIdToNamespace(spaceId)
+ : undefined;
+ };
+
alertsClientFactory.initialize({
alertTypeRegistry: alertTypeRegistry!,
logger,
taskManager: plugins.taskManager,
securityPluginSetup: security,
encryptedSavedObjectsClient,
- spaceIdToNamespace: this.spaceIdToNamespace,
+ spaceIdToNamespace,
getSpaceId(request: KibanaRequest) {
- return spaces?.getSpaceId(request);
+ return plugins.spaces?.spacesService.getSpaceId(request);
},
async getSpace(request: KibanaRequest) {
- return spaces?.getActiveSpace(request);
+ return plugins.spaces?.spacesService.getActiveSpace(request);
},
actions: plugins.actions,
features: plugins.features,
@@ -296,7 +296,7 @@ export class AlertingPlugin {
const getAlertsClientWithRequest = (request: KibanaRequest) => {
if (isESOUsingEphemeralEncryptionKey === true) {
throw new Error(
- `Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml`
+ `Unable to create alerts client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.`
);
}
return alertsClientFactory!.create(request, core.savedObjects);
@@ -306,10 +306,10 @@ export class AlertingPlugin {
logger,
getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch),
getAlertsClientWithRequest,
- spaceIdToNamespace: this.spaceIdToNamespace,
+ spaceIdToNamespace,
actionsPlugin: plugins.actions,
encryptedSavedObjectsClient,
- getBasePath: this.getBasePath,
+ basePathService: core.http.basePath,
eventLogger: this.eventLogger!,
internalSavedObjectsRepository: core.savedObjects.createInternalRepository(['alert']),
});
@@ -363,14 +363,6 @@ export class AlertingPlugin {
});
}
- private spaceIdToNamespace = (spaceId?: string): string | undefined => {
- return this.spaces && spaceId ? this.spaces.spaceIdToNamespace(spaceId) : undefined;
- };
-
- private getBasePath = (spaceId?: string): string => {
- return this.spaces && spaceId ? this.spaces.getBasePath(spaceId) : this.serverBasePath!;
- };
-
private getScopedClientWithAlertSavedObjectType(
savedObjects: SavedObjectsServiceStart,
request: KibanaRequest
diff --git a/x-pack/plugins/alerts/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts
index da30273e93c6b..dfe122f56bc48 100644
--- a/x-pack/plugins/alerts/server/saved_objects/index.ts
+++ b/x-pack/plugins/alerts/server/saved_objects/index.ts
@@ -16,6 +16,7 @@ export const AlertAttributesExcludedFromAAD = [
'muteAll',
'mutedInstanceIds',
'updatedBy',
+ 'updatedAt',
'executionStatus',
];
@@ -28,6 +29,7 @@ export type AlertAttributesExcludedFromAADType =
| 'muteAll'
| 'mutedInstanceIds'
| 'updatedBy'
+ | 'updatedAt'
| 'executionStatus';
export function setupSavedObjects(
diff --git a/x-pack/plugins/alerts/server/saved_objects/mappings.json b/x-pack/plugins/alerts/server/saved_objects/mappings.json
index a6c92080f18be..f40a7d9075eed 100644
--- a/x-pack/plugins/alerts/server/saved_objects/mappings.json
+++ b/x-pack/plugins/alerts/server/saved_objects/mappings.json
@@ -62,6 +62,9 @@
"createdAt": {
"type": "date"
},
+ "updatedAt": {
+ "type": "date"
+ },
"apiKey": {
"type": "binary"
},
diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts
index 8c9d10769b18a..a4cbc18e13b47 100644
--- a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts
+++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts
@@ -261,8 +261,48 @@ describe('7.10.0 migrates with failure', () => {
});
});
+describe('7.11.0', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ encryptedSavedObjectsSetup.createMigration.mockImplementation(
+ (shouldMigrateWhenPredicate, migration) => migration
+ );
+ });
+
+ test('add updatedAt field to alert - set to SavedObject updated_at attribute', () => {
+ const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
+ const alert = getMockData({}, true);
+ expect(migration711(alert, { log })).toEqual({
+ ...alert,
+ attributes: {
+ ...alert.attributes,
+ updatedAt: alert.updated_at,
+ },
+ });
+ });
+
+ test('add updatedAt field to alert - set to createdAt when SavedObject updated_at is not defined', () => {
+ const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
+ const alert = getMockData({});
+ expect(migration711(alert, { log })).toEqual({
+ ...alert,
+ attributes: {
+ ...alert.attributes,
+ updatedAt: alert.attributes.createdAt,
+ },
+ });
+ });
+});
+
+function getUpdatedAt(): string {
+ const updatedAt = new Date();
+ updatedAt.setHours(updatedAt.getHours() + 2);
+ return updatedAt.toISOString();
+}
+
function getMockData(
- overwrites: Record = {}
+ overwrites: Record = {},
+ withSavedObjectUpdatedAt: boolean = false
): SavedObjectUnsanitizedDoc> {
return {
attributes: {
@@ -295,6 +335,7 @@ function getMockData(
],
...overwrites,
},
+ updated_at: withSavedObjectUpdatedAt ? getUpdatedAt() : undefined,
id: uuid.v4(),
type: 'alert',
};
diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts
index 0b2c86b84f67b..d8ebced03c5a6 100644
--- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts
+++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts
@@ -37,8 +37,15 @@ export function getMigrations(
)
);
+ const migrationAlertUpdatedAtDate = encryptedSavedObjects.createMigration(
+ // migrate all documents in 7.11 in order to add the "updatedAt" field
+ (doc): doc is SavedObjectUnsanitizedDoc => true,
+ pipeMigrations(setAlertUpdatedAtDate)
+ );
+
return {
'7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'),
+ '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtDate, '7.11.0'),
};
}
@@ -59,6 +66,19 @@ function executeMigrationWithErrorHandling(
};
}
+const setAlertUpdatedAtDate = (
+ doc: SavedObjectUnsanitizedDoc
+): SavedObjectUnsanitizedDoc => {
+ const updatedAt = doc.updated_at || doc.attributes.createdAt;
+ return {
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ updatedAt,
+ },
+ };
+};
+
const consumersToChange: Map = new Map(
Object.entries({
alerting: 'alerts',
diff --git a/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts
index 50815c797e399..8041ec551bb0d 100644
--- a/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts
+++ b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts
@@ -95,6 +95,7 @@ const DefaultAttributes = {
muteAll: true,
mutedInstanceIds: ['muted-instance-id-1', 'muted-instance-id-2'],
updatedBy: 'someone',
+ updatedAt: '2019-02-12T21:01:22.479Z',
};
const InvalidAttributes = { ...DefaultAttributes, foo: 'bar' };
diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts
index f49310c42c247..ccd1f6c20ba52 100644
--- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts
+++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts
@@ -75,6 +75,7 @@ export function createExecutionHandler({
spaceId,
tags,
alertInstanceId,
+ alertActionGroup: actionGroup,
context,
actionParams: action.params,
state,
diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts
index bd583159af5d5..07d08f5837d54 100644
--- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts
+++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts
@@ -18,6 +18,7 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv
import {
loggingSystemMock,
savedObjectsRepositoryMock,
+ httpServiceMock,
} from '../../../../../src/core/server/mocks';
import { PluginStartContract as ActionsPluginStart } from '../../../actions/server';
import { actionsMock, actionsClientMock } from '../../../actions/server/mocks';
@@ -78,7 +79,7 @@ describe('Task Runner', () => {
encryptedSavedObjectsClient,
logger: loggingSystemMock.create().get(),
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
- getBasePath: jest.fn().mockReturnValue(undefined),
+ basePathService: httpServiceMock.createBasePath(),
eventLogger: eventLoggerMock.create(),
internalSavedObjectsRepository: savedObjectsRepositoryMock.create(),
};
@@ -375,23 +376,24 @@ describe('Task Runner', () => {
await taskRunner.run();
expect(
taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest
- ).toHaveBeenCalledWith({
- getBasePath: expect.anything(),
- headers: {
- // base64 encoded "123:abc"
- authorization: 'ApiKey MTIzOmFiYw==',
- },
- path: '/',
- route: { settings: {} },
- url: {
- href: '/',
- },
- raw: {
- req: {
- url: '/',
+ ).toHaveBeenCalledWith(
+ expect.objectContaining({
+ headers: {
+ // base64 encoded "123:abc"
+ authorization: 'ApiKey MTIzOmFiYw==',
},
- },
- });
+ })
+ );
+
+ const [
+ request,
+ ] = taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest.mock.calls[0];
+
+ expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
+ request,
+ '/'
+ );
+
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1);
expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(`
Array [
@@ -768,23 +770,20 @@ describe('Task Runner', () => {
});
await taskRunner.run();
- expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith({
- getBasePath: expect.anything(),
- headers: {
- // base64 encoded "123:abc"
- authorization: 'ApiKey MTIzOmFiYw==',
- },
- path: '/',
- route: { settings: {} },
- url: {
- href: '/',
- },
- raw: {
- req: {
- url: '/',
+ expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith(
+ expect.objectContaining({
+ headers: {
+ // base64 encoded "123:abc"
+ authorization: 'ApiKey MTIzOmFiYw==',
},
- },
- });
+ })
+ );
+ const [request] = taskRunnerFactoryInitializerParams.getServices.mock.calls[0];
+
+ expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
+ request,
+ '/'
+ );
});
test(`doesn't use API key when not provided`, async () => {
@@ -803,20 +802,18 @@ describe('Task Runner', () => {
await taskRunner.run();
- expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith({
- getBasePath: expect.anything(),
- headers: {},
- path: '/',
- route: { settings: {} },
- url: {
- href: '/',
- },
- raw: {
- req: {
- url: '/',
- },
- },
- });
+ expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith(
+ expect.objectContaining({
+ headers: {},
+ })
+ );
+
+ const [request] = taskRunnerFactoryInitializerParams.getServices.mock.calls[0];
+
+ expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
+ request,
+ '/'
+ );
});
test('rescheduled the Alert if the schedule has update during a task run', async () => {
diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts
index 0dad952a86590..24d96788c3395 100644
--- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts
+++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts
@@ -5,6 +5,8 @@
*/
import type { PublicMethodsOf } from '@kbn/utility-types';
import { Dictionary, pickBy, mapValues, without, cloneDeep } from 'lodash';
+import type { Request } from '@hapi/hapi';
+import { addSpaceIdToPath } from '../../../spaces/server';
import { Logger, KibanaRequest } from '../../../../../src/core/server';
import { TaskRunnerContext } from './task_runner_factory';
import { ConcreteTaskInstance, throwUnrecoverableError } from '../../../task_manager/server';
@@ -91,9 +93,10 @@ export class TaskRunner {
requestHeaders.authorization = `ApiKey ${apiKey}`;
}
- return ({
+ const path = addSpaceIdToPath('/', spaceId);
+
+ const fakeRequest = KibanaRequest.from(({
headers: requestHeaders,
- getBasePath: () => this.context.getBasePath(spaceId),
path: '/',
route: { settings: {} },
url: {
@@ -104,7 +107,11 @@ export class TaskRunner {
url: '/',
},
},
- } as unknown) as KibanaRequest;
+ } as unknown) as Request);
+
+ this.context.basePathService.set(fakeRequest, path);
+
+ return fakeRequest;
}
private getServicesWithSpaceLevelPermissions(
diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts
index 5da8e4296f4dd..1c10a997d8cdd 100644
--- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts
+++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts
@@ -11,6 +11,7 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv
import {
loggingSystemMock,
savedObjectsRepositoryMock,
+ httpServiceMock,
} from '../../../../../src/core/server/mocks';
import { actionsMock } from '../../../actions/server/mocks';
import { alertsMock, alertsClientMock } from '../mocks';
@@ -64,7 +65,7 @@ describe('Task Runner Factory', () => {
encryptedSavedObjectsClient: encryptedSavedObjectsPlugin.getClient(),
logger: loggingSystemMock.create().get(),
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
- getBasePath: jest.fn().mockReturnValue(undefined),
+ basePathService: httpServiceMock.createBasePath(),
eventLogger: eventLoggerMock.create(),
internalSavedObjectsRepository: savedObjectsRepositoryMock.create(),
};
diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts
index df6f306c6ccc5..2a2d74c1fc259 100644
--- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts
+++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts
@@ -4,16 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
import type { PublicMethodsOf } from '@kbn/utility-types';
-import { Logger, KibanaRequest, ISavedObjectsRepository } from '../../../../../src/core/server';
+import {
+ Logger,
+ KibanaRequest,
+ ISavedObjectsRepository,
+ IBasePath,
+} from '../../../../../src/core/server';
import { RunContext } from '../../../task_manager/server';
import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server';
import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server';
-import {
- AlertType,
- GetBasePathFunction,
- GetServicesFunction,
- SpaceIdToNamespaceFunction,
-} from '../types';
+import { AlertType, GetServicesFunction, SpaceIdToNamespaceFunction } from '../types';
import { TaskRunner } from './task_runner';
import { IEventLogger } from '../../../event_log/server';
import { AlertsClient } from '../alerts_client';
@@ -26,7 +26,7 @@ export interface TaskRunnerContext {
eventLogger: IEventLogger;
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
spaceIdToNamespace: SpaceIdToNamespaceFunction;
- getBasePath: GetBasePathFunction;
+ basePathService: IBasePath;
internalSavedObjectsRepository: ISavedObjectsRepository;
}
diff --git a/x-pack/plugins/alerts/server/task_runner/transform_action_params.test.ts b/x-pack/plugins/alerts/server/task_runner/transform_action_params.test.ts
index ddbef8e32e708..9a4cfbbca792d 100644
--- a/x-pack/plugins/alerts/server/task_runner/transform_action_params.test.ts
+++ b/x-pack/plugins/alerts/server/task_runner/transform_action_params.test.ts
@@ -24,6 +24,7 @@ test('skips non string parameters', () => {
tags: ['tag-A', 'tag-B'],
spaceId: 'spaceId-A',
alertInstanceId: '2',
+ alertActionGroup: 'action-group',
alertParams: {
foo: 'test',
},
@@ -54,6 +55,7 @@ test('missing parameters get emptied out', () => {
tags: ['tag-A', 'tag-B'],
spaceId: 'spaceId-A',
alertInstanceId: '2',
+ alertActionGroup: 'action-group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@@ -77,6 +79,7 @@ test('context parameters are passed to templates', () => {
tags: ['tag-A', 'tag-B'],
spaceId: 'spaceId-A',
alertInstanceId: '2',
+ alertActionGroup: 'action-group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@@ -99,6 +102,7 @@ test('state parameters are passed to templates', () => {
tags: ['tag-A', 'tag-B'],
spaceId: 'spaceId-A',
alertInstanceId: '2',
+ alertActionGroup: 'action-group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@@ -121,6 +125,7 @@ test('alertId is passed to templates', () => {
tags: ['tag-A', 'tag-B'],
spaceId: 'spaceId-A',
alertInstanceId: '2',
+ alertActionGroup: 'action-group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@@ -143,6 +148,7 @@ test('alertName is passed to templates', () => {
tags: ['tag-A', 'tag-B'],
spaceId: 'spaceId-A',
alertInstanceId: '2',
+ alertActionGroup: 'action-group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@@ -165,6 +171,7 @@ test('tags is passed to templates', () => {
tags: ['tag-A', 'tag-B'],
spaceId: 'spaceId-A',
alertInstanceId: '2',
+ alertActionGroup: 'action-group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@@ -186,6 +193,7 @@ test('undefined tags is passed to templates', () => {
alertName: 'alert-name',
spaceId: 'spaceId-A',
alertInstanceId: '2',
+ alertActionGroup: 'action-group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@@ -208,6 +216,7 @@ test('empty tags is passed to templates', () => {
tags: [],
spaceId: 'spaceId-A',
alertInstanceId: '2',
+ alertActionGroup: 'action-group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@@ -230,6 +239,7 @@ test('spaceId is passed to templates', () => {
tags: ['tag-A', 'tag-B'],
spaceId: 'spaceId-A',
alertInstanceId: '2',
+ alertActionGroup: 'action-group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@@ -252,6 +262,7 @@ test('alertInstanceId is passed to templates', () => {
tags: ['tag-A', 'tag-B'],
spaceId: 'spaceId-A',
alertInstanceId: '2',
+ alertActionGroup: 'action-group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@@ -261,6 +272,53 @@ test('alertInstanceId is passed to templates', () => {
`);
});
+test('alertActionGroup is passed to templates', () => {
+ const actionParams = {
+ message: 'Value "{{alertActionGroup}}" exists',
+ };
+ const result = transformActionParams({
+ actionParams,
+ state: {},
+ context: {},
+ alertId: '1',
+ alertName: 'alert-name',
+ tags: ['tag-A', 'tag-B'],
+ spaceId: 'spaceId-A',
+ alertInstanceId: '2',
+ alertActionGroup: 'action-group',
+ alertParams: {},
+ });
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "message": "Value \\"action-group\\" exists",
+ }
+ `);
+});
+
+test('date is passed to templates', () => {
+ const actionParams = {
+ message: '{{date}}',
+ };
+ const dateBefore = Date.now();
+ const result = transformActionParams({
+ actionParams,
+ state: {},
+ context: {},
+ alertId: '1',
+ alertName: 'alert-name',
+ tags: ['tag-A', 'tag-B'],
+ spaceId: 'spaceId-A',
+ alertInstanceId: '2',
+ alertActionGroup: 'action-group',
+ alertParams: {},
+ });
+ const dateAfter = Date.now();
+ const dateVariable = new Date(`${result.message}`).valueOf();
+
+ expect(dateVariable).toBeGreaterThanOrEqual(dateBefore);
+ expect(dateVariable).toBeLessThanOrEqual(dateAfter);
+});
+
test('works recursively', () => {
const actionParams = {
body: {
@@ -276,6 +334,7 @@ test('works recursively', () => {
tags: ['tag-A', 'tag-B'],
spaceId: 'spaceId-A',
alertInstanceId: '2',
+ alertActionGroup: 'action-group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@@ -302,6 +361,7 @@ test('works recursively with arrays', () => {
tags: ['tag-A', 'tag-B'],
spaceId: 'spaceId-A',
alertInstanceId: '2',
+ alertActionGroup: 'action-group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
diff --git a/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts b/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts
index 913fc51cb0f6e..b02285d56aa9a 100644
--- a/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts
+++ b/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts
@@ -19,6 +19,7 @@ interface TransformActionParamsOptions {
spaceId: string;
tags?: string[];
alertInstanceId: string;
+ alertActionGroup: string;
actionParams: AlertActionParams;
alertParams: AlertTypeParams;
state: AlertInstanceState;
@@ -31,6 +32,7 @@ export function transformActionParams({
spaceId,
tags,
alertInstanceId,
+ alertActionGroup,
context,
actionParams,
state,
@@ -48,7 +50,9 @@ export function transformActionParams({
spaceId,
tags,
alertInstanceId,
+ alertActionGroup,
context,
+ date: new Date().toISOString(),
state,
params: alertParams,
};
diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts
index dde1628156658..500c681a1d2b9 100644
--- a/x-pack/plugins/alerts/server/types.ts
+++ b/x-pack/plugins/alerts/server/types.ts
@@ -32,7 +32,6 @@ import {
export type WithoutQueryAndParams = Pick>;
export type GetServicesFunction = (request: KibanaRequest) => Services;
-export type GetBasePathFunction = (spaceId?: string) => string;
export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined;
declare module 'src/core/server' {
@@ -148,6 +147,7 @@ export interface RawAlert extends SavedObjectAttributes {
createdBy: string | null;
updatedBy: string | null;
createdAt: string;
+ updatedAt: string;
apiKey: string | null;
apiKeyOwner: string | null;
throttle: string | null;
diff --git a/x-pack/plugins/apm/common/runtime_types/merge/index.test.ts b/x-pack/plugins/apm/common/runtime_types/merge/index.test.ts
new file mode 100644
index 0000000000000..0e0cb4a349c83
--- /dev/null
+++ b/x-pack/plugins/apm/common/runtime_types/merge/index.test.ts
@@ -0,0 +1,71 @@
+/*
+ * 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 * as t from 'io-ts';
+import { isLeft } from 'fp-ts/lib/Either';
+import { merge } from './';
+import { jsonRt } from '../json_rt';
+
+describe('merge', () => {
+ it('fails on one or more errors', () => {
+ const type = merge([t.type({ foo: t.string }), t.type({ bar: t.number })]);
+
+ const result = type.decode({ foo: '' });
+
+ expect(isLeft(result)).toBe(true);
+ });
+
+ it('merges left to right', () => {
+ const typeBoolean = merge([
+ t.type({ foo: t.string }),
+ t.type({ foo: jsonRt.pipe(t.boolean) }),
+ ]);
+
+ const resultBoolean = typeBoolean.decode({
+ foo: 'true',
+ });
+
+ // @ts-expect-error
+ expect(resultBoolean.right).toEqual({
+ foo: true,
+ });
+
+ const typeString = merge([
+ t.type({ foo: jsonRt.pipe(t.boolean) }),
+ t.type({ foo: t.string }),
+ ]);
+
+ const resultString = typeString.decode({
+ foo: 'true',
+ });
+
+ // @ts-expect-error
+ expect(resultString.right).toEqual({
+ foo: 'true',
+ });
+ });
+
+ it('deeply merges values', () => {
+ const type = merge([
+ t.type({ foo: t.type({ baz: t.string }) }),
+ t.type({ foo: t.type({ bar: t.string }) }),
+ ]);
+
+ const result = type.decode({
+ foo: {
+ bar: '',
+ baz: '',
+ },
+ });
+
+ // @ts-expect-error
+ expect(result.right).toEqual({
+ foo: {
+ bar: '',
+ baz: '',
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/apm/common/runtime_types/merge/index.ts b/x-pack/plugins/apm/common/runtime_types/merge/index.ts
new file mode 100644
index 0000000000000..76a1092436dce
--- /dev/null
+++ b/x-pack/plugins/apm/common/runtime_types/merge/index.ts
@@ -0,0 +1,68 @@
+/*
+ * 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 * as t from 'io-ts';
+import { merge as lodashMerge } from 'lodash';
+import { isLeft } from 'fp-ts/lib/Either';
+import { ValuesType } from 'utility-types';
+
+export type MergeType<
+ T extends t.Any[],
+ U extends ValuesType = ValuesType
+> = t.Type & {
+ _tag: 'MergeType';
+ types: T;
+};
+
+// this is similar to t.intersection, but does a deep merge
+// instead of a shallow merge
+
+export function merge(
+ types: [A, B]
+): MergeType<[A, B]>;
+
+export function merge(types: t.Any[]) {
+ const mergeType = new t.Type(
+ 'merge',
+ (u): u is unknown => {
+ return types.every((type) => type.is(u));
+ },
+ (input, context) => {
+ const errors: t.Errors = [];
+
+ const successes: unknown[] = [];
+
+ const results = types.map((type, index) =>
+ type.validate(
+ input,
+ context.concat({
+ key: String(index),
+ type,
+ actual: input,
+ })
+ )
+ );
+
+ results.forEach((result) => {
+ if (isLeft(result)) {
+ errors.push(...result.left);
+ } else {
+ successes.push(result.right);
+ }
+ });
+
+ const mergedValues = lodashMerge({}, ...successes);
+
+ return errors.length > 0 ? t.failures(errors) : t.success(mergedValues);
+ },
+ (a) => types.reduce((val, type) => type.encode(val), a)
+ );
+
+ return {
+ ...mergeType,
+ _tag: 'MergeType',
+ types,
+ };
+}
diff --git a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts b/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts
new file mode 100644
index 0000000000000..ac2f7d8e1679a
--- /dev/null
+++ b/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts
@@ -0,0 +1,106 @@
+/*
+ * 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 * as t from 'io-ts';
+import { isRight, isLeft } from 'fp-ts/lib/Either';
+import { strictKeysRt } from './';
+import { jsonRt } from '../json_rt';
+
+describe('strictKeysRt', () => {
+ it('correctly and deeply validates object keys', () => {
+ const checks: Array<{ type: t.Type; passes: any[]; fails: any[] }> = [
+ {
+ type: t.intersection([
+ t.type({ foo: t.string }),
+ t.partial({ bar: t.string }),
+ ]),
+ passes: [{ foo: '' }, { foo: '', bar: '' }],
+ fails: [
+ { foo: '', unknownKey: '' },
+ { foo: '', bar: '', unknownKey: '' },
+ ],
+ },
+ {
+ type: t.type({
+ path: t.union([
+ t.type({ serviceName: t.string }),
+ t.type({ transactionType: t.string }),
+ ]),
+ }),
+ passes: [
+ { path: { serviceName: '' } },
+ { path: { transactionType: '' } },
+ ],
+ fails: [
+ { path: { serviceName: '', unknownKey: '' } },
+ { path: { transactionType: '', unknownKey: '' } },
+ { path: { serviceName: '', transactionType: '' } },
+ { path: { serviceName: '' }, unknownKey: '' },
+ ],
+ },
+ {
+ type: t.intersection([
+ t.type({ query: t.type({ bar: t.string }) }),
+ t.partial({ query: t.partial({ _debug: t.boolean }) }),
+ ]),
+ passes: [{ query: { bar: '', _debug: true } }],
+ fails: [{ query: { _debug: true } }],
+ },
+ ];
+
+ checks.forEach((check) => {
+ const { type, passes, fails } = check;
+
+ const strictType = strictKeysRt(type);
+
+ passes.forEach((value) => {
+ const result = strictType.decode(value);
+
+ if (!isRight(result)) {
+ throw new Error(
+ `Expected ${JSON.stringify(
+ value
+ )} to be allowed, but validation failed with ${
+ result.left[0].message
+ }`
+ );
+ }
+ });
+
+ fails.forEach((value) => {
+ const result = strictType.decode(value);
+
+ if (!isLeft(result)) {
+ throw new Error(
+ `Expected ${JSON.stringify(
+ value
+ )} to be disallowed, but validation succeeded`
+ );
+ }
+ });
+ });
+ });
+
+ it('does not support piped types', () => {
+ const typeA = t.type({
+ query: t.type({ filterNames: jsonRt.pipe(t.array(t.string)) }),
+ } as Record);
+
+ const typeB = t.partial({
+ query: t.partial({ _debug: jsonRt.pipe(t.boolean) }),
+ });
+
+ const value = {
+ query: {
+ _debug: 'true',
+ filterNames: JSON.stringify(['host', 'agentName']),
+ },
+ };
+
+ const pipedType = strictKeysRt(typeA.pipe(typeB));
+
+ expect(isLeft(pipedType.decode(value))).toBe(true);
+ });
+});
diff --git a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts b/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts
new file mode 100644
index 0000000000000..9ca37b4a0a26a
--- /dev/null
+++ b/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts
@@ -0,0 +1,195 @@
+/*
+ * 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 * as t from 'io-ts';
+import { either, isRight } from 'fp-ts/lib/Either';
+import { mapValues, difference, isPlainObject, forEach } from 'lodash';
+import { MergeType, merge } from '../merge';
+
+/*
+ Type that tracks validated keys, and fails when the input value
+ has keys that have not been validated.
+*/
+
+type ParsableType =
+ | t.IntersectionType
+ | t.UnionType
+ | t.PartialType
+ | t.ExactType
+ | t.InterfaceType
+ | MergeType;
+
+function getKeysInObject>(
+ object: T,
+ prefix: string = ''
+): string[] {
+ const keys: string[] = [];
+ forEach(object, (value, key) => {
+ const ownPrefix = prefix ? `${prefix}.${key}` : key;
+ keys.push(ownPrefix);
+ if (isPlainObject(object[key])) {
+ keys.push(
+ ...getKeysInObject(object[key] as Record, ownPrefix)
+ );
+ }
+ });
+ return keys;
+}
+
+function addToContextWhenValidated<
+ T extends t.InterfaceType | t.PartialType
+>(type: T, prefix: string): T {
+ const validate = (input: unknown, context: t.Context) => {
+ const result = type.validate(input, context);
+ const keysType = context[0].type as StrictKeysType;
+ if (!('trackedKeys' in keysType)) {
+ throw new Error('Expected a top-level StrictKeysType');
+ }
+ if (isRight(result)) {
+ keysType.trackedKeys.push(
+ ...Object.keys(type.props).map((propKey) => `${prefix}${propKey}`)
+ );
+ }
+ return result;
+ };
+
+ if (type._tag === 'InterfaceType') {
+ return new t.InterfaceType(
+ type.name,
+ type.is,
+ validate,
+ type.encode,
+ type.props
+ ) as T;
+ }
+
+ return new t.PartialType(
+ type.name,
+ type.is,
+ validate,
+ type.encode,
+ type.props
+ ) as T;
+}
+
+function trackKeysOfValidatedTypes(
+ type: ParsableType | t.Any,
+ prefix: string = ''
+): t.Any {
+ if (!('_tag' in type)) {
+ return type;
+ }
+ const taggedType = type as ParsableType;
+
+ switch (taggedType._tag) {
+ case 'IntersectionType': {
+ const collectionType = type as t.IntersectionType;
+ return t.intersection(
+ collectionType.types.map((rt) =>
+ trackKeysOfValidatedTypes(rt, prefix)
+ ) as [t.Any, t.Any]
+ );
+ }
+
+ case 'UnionType': {
+ const collectionType = type as t.UnionType;
+ return t.union(
+ collectionType.types.map((rt) =>
+ trackKeysOfValidatedTypes(rt, prefix)
+ ) as [t.Any, t.Any]
+ );
+ }
+
+ case 'MergeType': {
+ const collectionType = type as MergeType;
+ return merge(
+ collectionType.types.map((rt) =>
+ trackKeysOfValidatedTypes(rt, prefix)
+ ) as [t.Any, t.Any]
+ );
+ }
+
+ case 'PartialType': {
+ const propsType = type as t.PartialType;
+
+ return addToContextWhenValidated(
+ t.partial(
+ mapValues(propsType.props, (val, key) =>
+ trackKeysOfValidatedTypes(val, `${prefix}${key}.`)
+ )
+ ),
+ prefix
+ );
+ }
+
+ case 'InterfaceType': {
+ const propsType = type as t.InterfaceType;
+
+ return addToContextWhenValidated(
+ t.type(
+ mapValues(propsType.props, (val, key) =>
+ trackKeysOfValidatedTypes(val, `${prefix}${key}.`)
+ )
+ ),
+ prefix
+ );
+ }
+
+ case 'ExactType': {
+ const exactType = type as t.ExactType;
+
+ return t.exact(
+ trackKeysOfValidatedTypes(exactType.type, prefix) as t.HasProps
+ );
+ }
+
+ default:
+ return type;
+ }
+}
+
+class StrictKeysType<
+ A = any,
+ O = A,
+ I = any,
+ T extends t.Type = t.Type
+> extends t.Type {
+ trackedKeys: string[];
+
+ constructor(type: T) {
+ const trackedType = trackKeysOfValidatedTypes(type);
+
+ super(
+ 'strict_keys',
+ trackedType.is,
+ (input, context) => {
+ this.trackedKeys.length = 0;
+ return either.chain(trackedType.validate(input, context), (i) => {
+ const originalKeys = getKeysInObject(
+ input as Record
+ );
+ const excessKeys = difference(originalKeys, this.trackedKeys);
+
+ if (excessKeys.length) {
+ return t.failure(
+ i,
+ context,
+ `Excess keys are not allowed: \n${excessKeys.join('\n')}`
+ );
+ }
+
+ return t.success(i);
+ });
+ },
+ trackedType.encode
+ );
+
+ this.trackedKeys = [];
+ }
+}
+
+export function strictKeysRt(type: T): T {
+ return (new StrictKeysType(type) as unknown) as T;
+}
diff --git a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx
index d75446cb0dd48..e08bd01a1842b 100644
--- a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx
+++ b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx
@@ -24,8 +24,7 @@ import { APIReturnType } from '../../services/rest/createCallApmApi';
import { units } from '../../style/variables';
export type AnomalyDetectionApiResponse = APIReturnType<
- '/api/apm/settings/anomaly-detection',
- 'GET'
+ 'GET /api/apm/settings/anomaly-detection'
>;
const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false };
@@ -60,7 +59,7 @@ export function AnomalyDetectionSetupLink() {
export function MissingJobsAlert({ environment }: { environment?: string }) {
const { data = DEFAULT_DATA, status } = useFetcher(
(callApmApi) =>
- callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }),
+ callApmApi({ endpoint: `GET /api/apm/settings/anomaly-detection` }),
[],
{ preservePreviousData: false, showToastOnError: false }
);
diff --git a/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx
new file mode 100644
index 0000000000000..3ad71b52b6037
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx
@@ -0,0 +1,152 @@
+/*
+ * 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 {
+ ScaleType,
+ Chart,
+ LineSeries,
+ Axis,
+ CurveType,
+ Position,
+ timeFormatter,
+ Settings,
+} from '@elastic/charts';
+import React, { useState } from 'react';
+import { useParams } from 'react-router-dom';
+import { EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { useUrlParams } from '../../../hooks/useUrlParams';
+import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher';
+import {
+ APIReturnType,
+ callApmApi,
+} from '../../../services/rest/createCallApmApi';
+import { px } from '../../../style/variables';
+import { SignificantTermsTable } from './SignificantTermsTable';
+import { ChartContainer } from '../../shared/charts/chart_container';
+
+type CorrelationsApiResponse = NonNullable<
+ APIReturnType<'GET /api/apm/correlations/failed_transactions'>
+>;
+
+type SignificantTerm = NonNullable<
+ CorrelationsApiResponse['significantTerms']
+>[0];
+
+export function ErrorCorrelations() {
+ const [
+ selectedSignificantTerm,
+ setSelectedSignificantTerm,
+ ] = useState(null);
+
+ const { serviceName } = useParams<{ serviceName?: string }>();
+ const { urlParams, uiFilters } = useUrlParams();
+ const { transactionName, transactionType, start, end } = urlParams;
+
+ const { data, status } = useFetcher(() => {
+ if (start && end) {
+ return callApmApi({
+ endpoint: 'GET /api/apm/correlations/failed_transactions',
+ params: {
+ query: {
+ serviceName,
+ transactionName,
+ transactionType,
+ start,
+ end,
+ uiFilters: JSON.stringify(uiFilters),
+ fieldNames:
+ 'transaction.name,user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,kubernetes.pod.name,url.domain,container.id,service.node.name',
+ },
+ },
+ });
+ }
+ }, [serviceName, start, end, transactionName, transactionType, uiFilters]);
+
+ return (
+ <>
+
+
+
+ Error rate over time
+
+
+
+
+
+
+
+ >
+ );
+}
+
+function ErrorTimeseriesChart({
+ data,
+ selectedSignificantTerm,
+ status,
+}: {
+ data?: CorrelationsApiResponse;
+ selectedSignificantTerm: SignificantTerm | null;
+ status: FETCH_STATUS;
+}) {
+ const dateFormatter = timeFormatter('HH:mm:ss');
+
+ return (
+
+
+
+
+
+ `${roundFloat(d * 100)}%`}
+ />
+
+
+
+ {selectedSignificantTerm !== null ? (
+
+ ) : null}
+
+
+ );
+}
+
+function roundFloat(n: number, digits = 2) {
+ const factor = Math.pow(10, digits);
+ return Math.round(n * factor) / factor;
+}
diff --git a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx
new file mode 100644
index 0000000000000..4364731501b89
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx
@@ -0,0 +1,273 @@
+/*
+ * 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 {
+ ScaleType,
+ Chart,
+ LineSeries,
+ Axis,
+ CurveType,
+ BarSeries,
+ Position,
+ timeFormatter,
+ Settings,
+} from '@elastic/charts';
+import React, { useState } from 'react';
+import { useParams } from 'react-router-dom';
+import { EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { getDurationFormatter } from '../../../../common/utils/formatters';
+import { useUrlParams } from '../../../hooks/useUrlParams';
+import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher';
+import {
+ APIReturnType,
+ callApmApi,
+} from '../../../services/rest/createCallApmApi';
+import { SignificantTermsTable } from './SignificantTermsTable';
+import { ChartContainer } from '../../shared/charts/chart_container';
+
+type CorrelationsApiResponse = NonNullable<
+ APIReturnType<'GET /api/apm/correlations/slow_transactions'>
+>;
+
+type SignificantTerm = NonNullable<
+ CorrelationsApiResponse['significantTerms']
+>[0];
+
+export function LatencyCorrelations() {
+ const [
+ selectedSignificantTerm,
+ setSelectedSignificantTerm,
+ ] = useState(null);
+
+ const { serviceName } = useParams<{ serviceName?: string }>();
+ const { urlParams, uiFilters } = useUrlParams();
+ const { transactionName, transactionType, start, end } = urlParams;
+
+ const { data, status } = useFetcher(() => {
+ if (start && end) {
+ return callApmApi({
+ endpoint: 'GET /api/apm/correlations/slow_transactions',
+ params: {
+ query: {
+ serviceName,
+ transactionName,
+ transactionType,
+ start,
+ end,
+ uiFilters: JSON.stringify(uiFilters),
+ durationPercentile: '50',
+ fieldNames:
+ 'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,kubernetes.pod.name,url.domain,container.id,service.node.name',
+ },
+ },
+ });
+ }
+ }, [serviceName, start, end, transactionName, transactionType, uiFilters]);
+
+ return (
+ <>
+
+
+
+
+
+