diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0b0addf117f6f..de7159489689e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,6 +15,7 @@ /src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app /src/legacy/core_plugins/metrics/ @elastic/kibana-app /src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app +/src/legacy/core_plugins/vis_type_xy/ @elastic/kibana-app # Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon /src/plugins/home/public @elastic/kibana-app /src/plugins/home/server/*.ts @elastic/kibana-app diff --git a/.i18nrc.json b/.i18nrc.json index 1230151212f57..7d7685b5c1ef1 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -22,7 +22,10 @@ "interpreter": "src/legacy/core_plugins/interpreter", "kbn": "src/legacy/core_plugins/kibana", "kbnDocViews": "src/legacy/core_plugins/kbn_doc_views", - "management": ["src/legacy/core_plugins/management", "src/plugins/management"], + "management": [ + "src/legacy/core_plugins/management", + "src/plugins/management" + ], "kibana_react": "src/legacy/core_plugins/kibana_react", "kibana-react": "src/plugins/kibana_react", "kibana_utils": "src/plugins/kibana_utils", @@ -43,6 +46,7 @@ "visTypeTimeseries": ["src/legacy/core_plugins/vis_type_timeseries", "src/plugins/vis_type_timeseries"], "visTypeVega": "src/legacy/core_plugins/vis_type_vega", "visTypeVislib": "src/legacy/core_plugins/vis_type_vislib", + "visTypeXy": "src/legacy/core_plugins/vis_type_xy", "visualizations": [ "src/plugins/visualizations", "src/legacy/core_plugins/visualizations" diff --git a/.sass-lint.yml b/.sass-lint.yml index fba2c003484f6..9c64c1e5eea56 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -3,6 +3,7 @@ files: - 'src/legacy/core_plugins/metrics/**/*.s+(a|c)ss' - 'src/legacy/core_plugins/timelion/**/*.s+(a|c)ss' - 'src/legacy/core_plugins/vis_type_vislib/**/*.s+(a|c)ss' + - 'src/legacy/core_plugins/vis_type_xy/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/rollup/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/canvas/**/*.s+(a|c)ss' diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md index c61907f366301..2ebee16874c80 100644 --- a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md @@ -30,7 +30,7 @@ const provider: ConfigDeprecationProvider = ({ rename, unused }) => [ | Method | Description | | --- | --- | | [rename(oldKey, newKey)](./kibana-plugin-server.configdeprecationfactory.rename.md) | Rename a configuration property from inside a plugin's configuration path. Will log a deprecation warning if the oldKey was found and deprecation applied. | -| [renameFromRoot(oldKey, newKey)](./kibana-plugin-server.configdeprecationfactory.renamefromroot.md) | Rename a configuration property from the root configuration. Will log a deprecation warning if the oldKey was found and deprecation applied.This should be only used when renaming properties from different configuration's path. To rename properties from inside a plugin's configuration, use 'rename' instead. | +| [renameFromRoot(oldKey, newKey, silent)](./kibana-plugin-server.configdeprecationfactory.renamefromroot.md) | Rename a configuration property from the root configuration. Will log a deprecation warning if the oldKey was found and deprecation applied.This should be only used when renaming properties from different configuration's path. To rename properties from inside a plugin's configuration, use 'rename' instead. | | [unused(unusedKey)](./kibana-plugin-server.configdeprecationfactory.unused.md) | Remove a configuration property from inside a plugin's configuration path. Will log a deprecation warning if the unused key was found and deprecation applied. | | [unusedFromRoot(unusedKey)](./kibana-plugin-server.configdeprecationfactory.unusedfromroot.md) | Remove a configuration property from the root configuration. Will log a deprecation warning if the unused key was found and deprecation applied.This should be only used when removing properties from outside of a plugin's configuration. To remove properties from inside a plugin's configuration, use 'unused' instead. | diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md index 269f242ec35da..40ea891b17c95 100644 --- a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md @@ -11,7 +11,7 @@ This should be only used when renaming properties from different configuration's Signature: ```typescript -renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; +renameFromRoot(oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation; ``` ## Parameters @@ -20,6 +20,7 @@ renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; | --- | --- | --- | | oldKey | string | | | newKey | string | | +| silent | boolean | | Returns: diff --git a/docs/development/core/server/kibana-plugin-server.httpserverinfo.host.md b/docs/development/core/server/kibana-plugin-server.httpserverinfo.host.md new file mode 100644 index 0000000000000..ee7e1e5b7c9c9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserverinfo.host.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) > [host](./kibana-plugin-server.httpserverinfo.host.md) + +## HttpServerInfo.host property + +The hostname of the server + +Signature: + +```typescript +host: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserverinfo.md b/docs/development/core/server/kibana-plugin-server.httpserverinfo.md new file mode 100644 index 0000000000000..6dbdb11ddb66e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserverinfo.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) + +## HttpServerInfo interface + + +Signature: + +```typescript +export interface HttpServerInfo +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [host](./kibana-plugin-server.httpserverinfo.host.md) | string | The hostname of the server | +| [name](./kibana-plugin-server.httpserverinfo.name.md) | string | The name of the Kibana server | +| [port](./kibana-plugin-server.httpserverinfo.port.md) | number | The port the server is listening on | +| [protocol](./kibana-plugin-server.httpserverinfo.protocol.md) | 'http' | 'https' | 'socket' | The protocol used by the server | + diff --git a/docs/development/core/server/kibana-plugin-server.httpserverinfo.name.md b/docs/development/core/server/kibana-plugin-server.httpserverinfo.name.md new file mode 100644 index 0000000000000..8d3a45c90a342 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserverinfo.name.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) > [name](./kibana-plugin-server.httpserverinfo.name.md) + +## HttpServerInfo.name property + +The name of the Kibana server + +Signature: + +```typescript +name: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserverinfo.port.md b/docs/development/core/server/kibana-plugin-server.httpserverinfo.port.md new file mode 100644 index 0000000000000..5dd5a53830c44 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserverinfo.port.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) > [port](./kibana-plugin-server.httpserverinfo.port.md) + +## HttpServerInfo.port property + +The port the server is listening on + +Signature: + +```typescript +port: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserverinfo.protocol.md b/docs/development/core/server/kibana-plugin-server.httpserverinfo.protocol.md new file mode 100644 index 0000000000000..08afb5c3f7213 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserverinfo.protocol.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) > [protocol](./kibana-plugin-server.httpserverinfo.protocol.md) + +## HttpServerInfo.protocol property + +The protocol used by the server + +Signature: + +```typescript +protocol: 'http' | 'https' | 'socket'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.getserverinfo.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.getserverinfo.md new file mode 100644 index 0000000000000..4501a7e26f75f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.getserverinfo.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) > [getServerInfo](./kibana-plugin-server.httpservicesetup.getserverinfo.md) + +## HttpServiceSetup.getServerInfo property + +Provides common [information](./kibana-plugin-server.httpserverinfo.md) about the running http server. + +Signature: + +```typescript +getServerInfo: () => HttpServerInfo; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md index 2a4b0e09977c1..c2d53ec1eaf52 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md @@ -86,6 +86,7 @@ async (context, request, response) => { | [createCookieSessionStorageFactory](./kibana-plugin-server.httpservicesetup.createcookiesessionstoragefactory.md) | <T>(cookieOptions: SessionStorageCookieOptions<T>) => Promise<SessionStorageFactory<T>> | Creates cookie based session storage factory [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | | [createRouter](./kibana-plugin-server.httpservicesetup.createrouter.md) | () => IRouter | Provides ability to declare a handler function for a particular path and HTTP request method. | | [csp](./kibana-plugin-server.httpservicesetup.csp.md) | ICspConfig | The CSP config used for Kibana. | +| [getServerInfo](./kibana-plugin-server.httpservicesetup.getserverinfo.md) | () => HttpServerInfo | Provides common [information](./kibana-plugin-server.httpserverinfo.md) about the running http server. | | [isTlsEnabled](./kibana-plugin-server.httpservicesetup.istlsenabled.md) | boolean | Flag showing whether a server was configured to use TLS connection. | | [registerAuth](./kibana-plugin-server.httpservicesetup.registerauth.md) | (handler: AuthenticationHandler) => void | To define custom authentication and/or authorization mechanism for incoming requests. | | [registerOnPostAuth](./kibana-plugin-server.httpservicesetup.registeronpostauth.md) | (handler: OnPostAuthHandler) => void | To define custom logic to perform for incoming requests. | diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index d6fba44fc9469..e695ce53df2ab 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -65,6 +65,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ErrorHttpResponseOptions](./kibana-plugin-server.errorhttpresponseoptions.md) | HTTP response parameters | | [FakeRequest](./kibana-plugin-server.fakerequest.md) | Fake request object created manually by Kibana plugins. | | [HttpResponseOptions](./kibana-plugin-server.httpresponseoptions.md) | HTTP response parameters | +| [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) | | | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to hapi server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. | | [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | | | [IContextContainer](./kibana-plugin-server.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | diff --git a/package.json b/package.json index e249546e71581..ff6d32bfc39e5 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "@elastic/charts": "^16.1.0", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "7.6.0", - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.3.3", diff --git a/packages/kbn-plugin-generator/index.js b/packages/kbn-plugin-generator/index.js index 90274288357b8..15adce7f01c8e 100644 --- a/packages/kbn-plugin-generator/index.js +++ b/packages/kbn-plugin-generator/index.js @@ -29,6 +29,7 @@ exports.run = function run(argv) { const options = getopts(argv, { alias: { h: 'help', + i: 'internal', }, }); @@ -40,17 +41,22 @@ exports.run = function run(argv) { if (options.help) { console.log( dedent(chalk` - {dim usage:} node scripts/generate-plugin {bold [name]} - - generate a fresh Kibana plugin in the plugins/ directory + # {dim Usage:} + node scripts/generate-plugin {bold [name]} + Generate a fresh Kibana plugin in the plugins/ directory + + # {dim Core Kibana plugins:} + node scripts/generate-plugin {bold [name]} -i + To generate a core Kibana plugin inside the src/plugins/ directory, add the -i flag. `) + '\n' ); process.exit(1); } const name = options._[0]; + const isKibanaPlugin = options.internal; const template = resolve(__dirname, './sao_template'); - const kibanaPlugins = resolve(__dirname, '../../plugins'); + const kibanaPlugins = resolve(__dirname, isKibanaPlugin ? '../../src/plugins' : '../../plugins'); const targetPath = resolve(kibanaPlugins, snakeCase(name)); sao({ @@ -58,6 +64,8 @@ exports.run = function run(argv) { targetPath: targetPath, configOptions: { name, + isKibanaPlugin, + targetPath, }, }).catch(error => { console.error(chalk`{red fatal error}!`); diff --git a/packages/kbn-plugin-generator/index.js.d.ts b/packages/kbn-plugin-generator/index.js.d.ts new file mode 100644 index 0000000000000..46f7c43fd5790 --- /dev/null +++ b/packages/kbn-plugin-generator/index.js.d.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ +interface PluginGenerator { + /** + * Run plugin generator. + */ + run: (...args: any[]) => any; +} diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index aa6611f3b6738..129125c4583d5 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -61,7 +61,8 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug expect(stats.isDirectory()).toBe(true); }); - it(`should create an internationalization config file with a blank line appended to satisfy the parser`, async () => { + // skipped until internationalization is re-introduced + it.skip(`should create an internationalization config file with a blank line appended to satisfy the parser`, async () => { // Link to the error that happens when the blank line is not there: // https://github.com/elastic/kibana/pull/45044#issuecomment-530092627 const intlFile = `${generatedPath}/.i18nrc.json`; @@ -78,16 +79,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug }); }); - it(`'yarn test:server' should exit 0`, async () => { - await execa('yarn', ['test:server'], { - cwd: generatedPath, - env: { - DISABLE_JUNIT_REPORTER: '1', - }, - }); - }); - - it(`'yarn build' should exit 0`, async () => { + it.skip(`'yarn build' should exit 0`, async () => { await execa('yarn', ['build'], { cwd: generatedPath }); }); @@ -109,7 +101,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug '--migrations.skip=true', ], cwd: generatedPath, - wait: /ispec_plugin.+Status changed from uninitialized to green - Ready/, + wait: new RegExp('\\[ispecPlugin\\]\\[plugins\\] Setting up plugin'), }); await proc.stop('kibana'); }); @@ -120,7 +112,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug await execa('yarn', ['preinstall'], { cwd: generatedPath }); }); - it(`'yarn lint' should exit 0`, async () => { + it.skip(`'yarn lint' should exit 0`, async () => { await execa('yarn', ['lint'], { cwd: generatedPath }); }); diff --git a/packages/kbn-plugin-generator/sao_template/sao.js b/packages/kbn-plugin-generator/sao_template/sao.js index f7401cba84358..aed4b9a02838f 100755 --- a/packages/kbn-plugin-generator/sao_template/sao.js +++ b/packages/kbn-plugin-generator/sao_template/sao.js @@ -17,21 +17,19 @@ * under the License. */ -const { resolve, relative, dirname } = require('path'); +const { relative } = require('path'); const startCase = require('lodash.startcase'); const camelCase = require('lodash.camelcase'); const snakeCase = require('lodash.snakecase'); -const execa = require('execa'); const chalk = require('chalk'); +const execa = require('execa'); const pkg = require('../package.json'); const kibanaPkgPath = require.resolve('../../../package.json'); const kibanaPkg = require(kibanaPkgPath); // eslint-disable-line import/no-dynamic-require -const KBN_DIR = dirname(kibanaPkgPath); - -module.exports = function({ name }) { +module.exports = function({ name, targetPath, isKibanaPlugin }) { return { prompts: { description: { @@ -47,41 +45,38 @@ module.exports = function({ name }) { message: 'Should an app component be generated?', default: true, }, - generateTranslations: { - type: 'confirm', - message: 'Should translation files be generated?', - default: true, - }, - generateHack: { - type: 'confirm', - message: 'Should a hack component be generated?', - default: true, - }, generateApi: { type: 'confirm', message: 'Should a server API be generated?', default: true, }, + // generateTranslations: { + // type: 'confirm', + // message: 'Should translation files be generated?', + // default: true, + // }, generateScss: { type: 'confirm', message: 'Should SCSS be used?', when: answers => answers.generateApp, default: true, }, + generateEslint: { + type: 'confirm', + message: 'Would you like to use a custom eslint file?', + default: !isKibanaPlugin, + }, }, filters: { + 'public/**/index.scss': 'generateScss', 'public/**/*': 'generateApp', - 'translations/**/*': 'generateTranslations', - '.i18nrc.json': 'generateTranslations', - 'public/hack.js': 'generateHack', 'server/**/*': 'generateApi', - 'public/app.scss': 'generateScss', - '.kibana-plugin-helpers.json': 'generateScss', + // 'translations/**/*': 'generateTranslations', + // '.i18nrc.json': 'generateTranslations', + 'eslintrc.js': 'generateEslint', }, move: { - gitignore: '.gitignore', 'eslintrc.js': '.eslintrc.js', - 'package_template.json': 'package.json', }, data: answers => Object.assign( @@ -91,34 +86,36 @@ module.exports = function({ name }) { camelCase, snakeCase, name, + isKibanaPlugin, + kbnVersion: answers.kbnVersion, + upperCamelCaseName: name.charAt(0).toUpperCase() + camelCase(name).slice(1), + hasUi: !!answers.generateApp, + hasServer: !!answers.generateApi, + hasScss: !!answers.generateScss, + relRoot: isKibanaPlugin ? '../../../..' : '../../..', }, answers ), enforceNewFolder: true, installDependencies: false, - gitInit: true, + gitInit: !isKibanaPlugin, async post({ log }) { - await execa('yarn', ['kbn', 'bootstrap'], { - cwd: KBN_DIR, - stdio: 'inherit', - }); - - const dir = relative(process.cwd(), resolve(KBN_DIR, 'plugins', snakeCase(name))); + const dir = relative(process.cwd(), targetPath); + // Apply eslint to the generated plugin try { - await execa('yarn', ['lint', '--fix'], { - cwd: dir, - all: true, - }); + await execa('yarn', ['lint:es', `./${dir}/**/*.ts*`, '--no-ignore', '--fix']); } catch (error) { - throw new Error(`Failure when running prettier on the generated output: ${error.all}`); + console.error(error); + throw new Error( + `Failure when running prettier on the generated output: ${error.all || error}` + ); } log.success(chalk`🎉 -Your plugin has been created in {bold ${dir}}. Move into that directory to run it: +Your plugin has been created in {bold ${dir}}. - {bold cd "${dir}"} {bold yarn start} `); }, diff --git a/packages/kbn-plugin-generator/sao_template/sao.test.js b/packages/kbn-plugin-generator/sao_template/sao.test.js index 80149c008dad8..0dbdb7d3c097b 100755 --- a/packages/kbn-plugin-generator/sao_template/sao.test.js +++ b/packages/kbn-plugin-generator/sao_template/sao.test.js @@ -19,8 +19,6 @@ const sao = require('sao'); -const templatePkg = require('../package.json'); - const template = { fromPath: __dirname, configOptions: { @@ -32,121 +30,57 @@ function getFileContents(file) { return file.contents.toString(); } -function getConfig(file) { - const contents = getFileContents(file).replace(/\r?\n/gm, ''); - return contents.split('kibana.Plugin(')[1]; -} - describe('plugin generator sao integration', () => { test('skips files when answering no', async () => { const res = await sao.mockPrompt(template, { generateApp: false, - generateHack: false, generateApi: false, }); - expect(res.fileList).not.toContain('public/app.js'); - expect(res.fileList).not.toContain('public/__tests__/index.js'); - expect(res.fileList).not.toContain('public/hack.js'); - expect(res.fileList).not.toContain('server/routes/example.js'); - expect(res.fileList).not.toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).not.toContain('app:'); - expect(uiExports).not.toContain('hacks:'); - expect(uiExports).not.toContain('init(server, options)'); - expect(uiExports).not.toContain('registerFeature('); + expect(res.fileList).toContain('common/index.ts'); + expect(res.fileList).not.toContain('public/index.ts'); + expect(res.fileList).not.toContain('server/index.ts'); }); it('includes app when answering yes', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: false, - generateApi: false, - }); - - // check output files - expect(res.fileList).toContain('public/app.js'); - expect(res.fileList).toContain('public/__tests__/index.js'); - expect(res.fileList).not.toContain('public/hack.js'); - expect(res.fileList).not.toContain('server/routes/example.js'); - expect(res.fileList).not.toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).toContain('app:'); - expect(uiExports).toContain('init(server, options)'); - expect(uiExports).toContain('registerFeature('); - expect(uiExports).not.toContain('hacks:'); - }); - - it('includes hack when answering yes', async () => { - const res = await sao.mockPrompt(template, { - generateApp: true, - generateHack: true, generateApi: false, }); // check output files - expect(res.fileList).toContain('public/app.js'); - expect(res.fileList).toContain('public/__tests__/index.js'); - expect(res.fileList).toContain('public/hack.js'); - expect(res.fileList).not.toContain('server/routes/example.js'); - expect(res.fileList).not.toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).toContain('app:'); - expect(uiExports).toContain('hacks:'); - expect(uiExports).toContain('init(server, options)'); - expect(uiExports).toContain('registerFeature('); + expect(res.fileList).toContain('common/index.ts'); + expect(res.fileList).toContain('public/index.ts'); + expect(res.fileList).toContain('public/plugin.ts'); + expect(res.fileList).toContain('public/types.ts'); + expect(res.fileList).toContain('public/components/app.tsx'); + expect(res.fileList).not.toContain('server/index.ts'); }); it('includes server api when answering yes', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: true, generateApi: true, }); // check output files - expect(res.fileList).toContain('public/app.js'); - expect(res.fileList).toContain('public/__tests__/index.js'); - expect(res.fileList).toContain('public/hack.js'); - expect(res.fileList).toContain('server/routes/example.js'); - expect(res.fileList).toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).toContain('app:'); - expect(uiExports).toContain('hacks:'); - expect(uiExports).toContain('init(server, options)'); - expect(uiExports).toContain('registerFeature('); - }); - - it('plugin config has correct name and main path', async () => { - const res = await sao.mockPrompt(template, { - generateApp: true, - generateHack: true, - generateApi: true, - }); - - const indexContents = getFileContents(res.files['index.js']); - const nameLine = indexContents.match('name: (.*)')[1]; - const mainLine = indexContents.match('main: (.*)')[1]; - - expect(nameLine).toContain('some_fancy_plugin'); - expect(mainLine).toContain('plugins/some_fancy_plugin/app'); + expect(res.fileList).toContain('public/plugin.ts'); + expect(res.fileList).toContain('server/plugin.ts'); + expect(res.fileList).toContain('server/index.ts'); + expect(res.fileList).toContain('server/types.ts'); + expect(res.fileList).toContain('server/routes/index.ts'); }); - it('plugin package has correct name', async () => { + it('plugin package has correct title', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: true, generateApi: true, }); - const packageContents = getFileContents(res.files['package.json']); - const pkg = JSON.parse(packageContents); + const contents = getFileContents(res.files['common/index.ts']); + const controllerLine = contents.match("PLUGIN_NAME = '(.*)'")[1]; - expect(pkg.name).toBe('some_fancy_plugin'); + expect(controllerLine).toContain('Some fancy plugin'); }); it('package has version "kibana" with master', async () => { @@ -154,10 +88,10 @@ describe('plugin generator sao integration', () => { kbnVersion: 'master', }); - const packageContents = getFileContents(res.files['package.json']); + const packageContents = getFileContents(res.files['kibana.json']); const pkg = JSON.parse(packageContents); - expect(pkg.kibana.version).toBe('kibana'); + expect(pkg.version).toBe('master'); }); it('package has correct version', async () => { @@ -165,39 +99,26 @@ describe('plugin generator sao integration', () => { kbnVersion: 'v6.0.0', }); - const packageContents = getFileContents(res.files['package.json']); - const pkg = JSON.parse(packageContents); - - expect(pkg.kibana.version).toBe('v6.0.0'); - }); - - it('package has correct templateVersion', async () => { - const res = await sao.mockPrompt(template, { - kbnVersion: 'master', - }); - - const packageContents = getFileContents(res.files['package.json']); + const packageContents = getFileContents(res.files['kibana.json']); const pkg = JSON.parse(packageContents); - expect(pkg.kibana.templateVersion).toBe(templatePkg.version); + expect(pkg.version).toBe('v6.0.0'); }); it('sample app has correct values', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: true, generateApi: true, }); - const contents = getFileContents(res.files['public/app.js']); - const controllerLine = contents.match('setRootController(.*)')[1]; + const contents = getFileContents(res.files['common/index.ts']); + const controllerLine = contents.match("PLUGIN_ID = '(.*)'")[1]; expect(controllerLine).toContain('someFancyPlugin'); }); it('includes dotfiles', async () => { const res = await sao.mockPrompt(template); - expect(res.files['.gitignore']).toBeTruthy(); expect(res.files['.eslintrc.js']).toBeTruthy(); }); }); diff --git a/packages/kbn-plugin-generator/sao_template/template/.i18nrc.json b/packages/kbn-plugin-generator/sao_template/template/.i18nrc.json deleted file mode 100644 index 1a8aea8853876..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/.i18nrc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "paths": { - "<%= camelCase(name) %>": "./" - }, - "translations": [ - "translations/zh-CN.json" - ] -} - diff --git a/packages/kbn-plugin-generator/sao_template/template/.kibana-plugin-helpers.json b/packages/kbn-plugin-generator/sao_template/template/.kibana-plugin-helpers.json deleted file mode 100644 index 383368c7f8ce1..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/.kibana-plugin-helpers.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "styleSheetToCompile": "public/app.scss" -} diff --git a/packages/kbn-plugin-generator/sao_template/template/README.md b/packages/kbn-plugin-generator/sao_template/template/README.md index 59c3adf2713c8..1e0139428fcbc 100755 --- a/packages/kbn-plugin-generator/sao_template/template/README.md +++ b/packages/kbn-plugin-generator/sao_template/template/README.md @@ -6,34 +6,7 @@ --- -## development +## Development -See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. Once you have completed that, use the following yarn scripts. +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. - - `yarn kbn bootstrap` - - Install dependencies and crosslink Kibana and all projects/plugins. - - > ***IMPORTANT:*** Use this script instead of `yarn` to install dependencies when switching branches, and re-run it whenever your dependencies change. - - - `yarn start` - - Start kibana and have it include this plugin. You can pass any arguments that you would normally send to `bin/kibana` - - ``` - yarn start --elasticsearch.hosts http://localhost:9220 - ``` - - - `yarn build` - - Build a distributable archive of your plugin. - - - `yarn test:browser` - - Run the browser tests in a real web browser. - - - `yarn test:mocha` - - Run the server tests using mocha. - -For more information about any of these commands run `yarn ${task} --help`. For a full list of tasks checkout the `package.json` file, or run `yarn run`. diff --git a/packages/kbn-plugin-generator/sao_template/template/common/index.ts b/packages/kbn-plugin-generator/sao_template/template/common/index.ts new file mode 100644 index 0000000000000..90ffcb70045aa --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/common/index.ts @@ -0,0 +1,2 @@ +export const PLUGIN_ID = '<%= camelCase(name) %>'; +export const PLUGIN_NAME = '<%= name %>'; diff --git a/packages/kbn-plugin-generator/sao_template/template/eslintrc.js b/packages/kbn-plugin-generator/sao_template/template/eslintrc.js old mode 100755 new mode 100644 index e1dfadc212b7e..b68d42e32e047 --- a/packages/kbn-plugin-generator/sao_template/template/eslintrc.js +++ b/packages/kbn-plugin-generator/sao_template/template/eslintrc.js @@ -1,24 +1,9 @@ -module.exports = { - root: true, +module.exports = { + root: true, extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], - settings: { - 'import/resolver': { - '@kbn/eslint-import-resolver-kibana': { - rootPackageName: '<%= snakeCase(name) %>', - }, - }, - }, - overrides: [ - { - files: ['**/public/**/*'], - settings: { - 'import/resolver': { - '@kbn/eslint-import-resolver-kibana': { - forceNode: false, - rootPackageName: '<%= snakeCase(name) %>', - }, - }, - }, - }, - ] -}; + <%_ if (!isKibanaPlugin) { -%> + rules: { + "@kbn/eslint/require-license-header": "off" + } + <%_ } -%> +}; \ No newline at end of file diff --git a/packages/kbn-plugin-generator/sao_template/template/gitignore b/packages/kbn-plugin-generator/sao_template/template/gitignore deleted file mode 100755 index db28fed19376d..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/gitignore +++ /dev/null @@ -1,6 +0,0 @@ -npm-debug.log* -node_modules -/build/ -<%_ if (generateScss) { -%> -/public/app.css -<%_ } -%> diff --git a/packages/kbn-plugin-generator/sao_template/template/index.js b/packages/kbn-plugin-generator/sao_template/template/index.js deleted file mode 100755 index 4bc3347ae6019..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/index.js +++ /dev/null @@ -1,89 +0,0 @@ -<% if (generateScss) { -%> -import { resolve } from 'path'; -import { existsSync } from 'fs'; - -<% } -%> - -<% if (generateApp) { -%> -import { i18n } from '@kbn/i18n'; -<% } -%> - -<% if (generateApi) { -%> -import exampleRoute from './server/routes/example'; - -<% } -%> -export default function (kibana) { - return new kibana.Plugin({ - require: ['elasticsearch'], - name: '<%= snakeCase(name) %>', - uiExports: { - <%_ if (generateApp) { -%> - app: { - title: '<%= startCase(name) %>', - description: '<%= description %>', - main: 'plugins/<%= snakeCase(name) %>/app', - }, - <%_ } -%> - <%_ if (generateHack) { -%> - hacks: [ - 'plugins/<%= snakeCase(name) %>/hack' - ], - <%_ } -%> - <%_ if (generateScss) { -%> - styleSheetPaths: [resolve(__dirname, 'public/app.scss'), resolve(__dirname, 'public/app.css')].find(p => existsSync(p)), - <%_ } -%> - }, - - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - <%_ if (generateApi || generateApp) { -%> - - // eslint-disable-next-line no-unused-vars - init(server, options) { - <%_ if (generateApp) { -%> - const xpackMainPlugin = server.plugins.xpack_main; - if (xpackMainPlugin) { - const featureId = '<%= snakeCase(name) %>'; - - xpackMainPlugin.registerFeature({ - id: featureId, - name: i18n.translate('<%= camelCase(name) %>.featureRegistry.featureName', { - defaultMessage: '<%= name %>', - }), - navLinkId: featureId, - icon: 'questionInCircle', - app: [featureId, 'kibana'], - catalogue: [], - privileges: { - all: { - api: [], - savedObject: { - all: [], - read: [], - }, - ui: ['show'], - }, - read: { - api: [], - savedObject: { - all: [], - read: [], - }, - ui: ['show'], - }, - }, - }); - } - <%_ } -%> - - <%_ if (generateApi) { -%> - // Add server routes and initialize the plugin here - exampleRoute(server); - <%_ } -%> - } - <%_ } -%> - }); -} diff --git a/packages/kbn-plugin-generator/sao_template/template/kibana.json b/packages/kbn-plugin-generator/sao_template/template/kibana.json new file mode 100644 index 0000000000000..f8bb07040abeb --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "<%= camelCase(name) %>", + "version": "<%= kbnVersion %>", + "server": <%= hasServer %>, + "ui": <%= hasUi %>, + "requiredPlugins": ["navigation"], + "optionalPlugins": [] +} diff --git a/packages/kbn-plugin-generator/sao_template/template/package_template.json b/packages/kbn-plugin-generator/sao_template/template/package_template.json deleted file mode 100644 index 4b6629fa90268..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/package_template.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "<%= snakeCase(name) %>", - "version": "0.0.0", - "description": "<%= description %>", - "main": "index.js", - "kibana": { - "version": "<%= (kbnVersion === 'master') ? 'kibana' : kbnVersion %>", - "templateVersion": "<%= templateVersion %>" - }, - "scripts": { - "preinstall": "node ../../preinstall_check", - "kbn": "node ../../scripts/kbn", - "es": "node ../../scripts/es", - "lint": "eslint .", - "start": "plugin-helpers start", - "test:server": "plugin-helpers test:server", - "test:browser": "plugin-helpers test:browser", - "build": "plugin-helpers build" - }, - <%_ if (generateTranslations) { _%> - "dependencies": { - "@kbn/i18n": "link:../../packages/kbn-i18n" - }, - <%_ } _%> - "devDependencies": { - "@elastic/eslint-config-kibana": "link:../../packages/eslint-config-kibana", - "@elastic/eslint-import-resolver-kibana": "link:../../packages/kbn-eslint-import-resolver-kibana", - "@kbn/expect": "link:../../packages/kbn-expect", - "@kbn/plugin-helpers": "link:../../packages/kbn-plugin-helpers", - "babel-eslint": "^10.0.1", - "eslint": "^5.16.0", - "eslint-plugin-babel": "^5.3.0", - "eslint-plugin-import": "^2.16.0", - "eslint-plugin-jest": "^22.4.1", - "eslint-plugin-jsx-a11y": "^6.2.1", - "eslint-plugin-mocha": "^5.3.0", - "eslint-plugin-no-unsanitized": "^3.0.2", - "eslint-plugin-prefer-object-spread": "^1.2.1", - "eslint-plugin-react": "^7.12.4" - } -} diff --git a/packages/kbn-plugin-generator/sao_template/template/public/__tests__/index.js b/packages/kbn-plugin-generator/sao_template/template/public/__tests__/index.js deleted file mode 100755 index 9320bd7b028a8..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/__tests__/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import expect from '@kbn/expect'; - -describe('suite', () => { - it('is a test', () => { - expect(true).to.equal(true); - }); -}); diff --git a/packages/kbn-plugin-generator/sao_template/template/public/app.js b/packages/kbn-plugin-generator/sao_template/template/public/app.js deleted file mode 100755 index 37a7c37e916a0..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/app.js +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; -import { render, unmountComponentAtNode } from 'react-dom'; -<%_ if (generateTranslations) { _%> -import { I18nProvider } from '@kbn/i18n/react'; -<%_ } _%> - -import { Main } from './components/main'; - -const app = uiModules.get('apps/<%= camelCase(name) %>'); - -app.config($locationProvider => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); -}); -app.config(stateManagementConfigProvider => - stateManagementConfigProvider.disable() -); - -function RootController($scope, $element, $http) { - const domNode = $element[0]; - - // render react to DOM - <%_ if (generateTranslations) { _%> - render( - -
- , - domNode - ); - <%_ } else { _%> - render(
, domNode); - <%_ } _%> - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); -} - -chrome.setRootController('<%= camelCase(name) %>', RootController); diff --git a/packages/kbn-plugin-generator/sao_template/template/public/application.tsx b/packages/kbn-plugin-generator/sao_template/template/public/application.tsx new file mode 100644 index 0000000000000..8106a18a784e7 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/application.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { AppMountParameters, CoreStart } from '<%= relRoot %>/src/core/public'; +import { AppPluginStartDependencies } from './types'; +import { <%= upperCamelCaseName %>App } from './components/app'; + + +export const renderApp = ( + { notifications, http }: CoreStart, + { navigation }: AppPluginStartDependencies, + { appBasePath, element }: AppMountParameters + ) => { + ReactDOM.render( + <<%= upperCamelCaseName %>App + basename={appBasePath} + notifications={notifications} + http={http} + navigation={navigation} + />, + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); + }; + \ No newline at end of file diff --git a/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx b/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx new file mode 100644 index 0000000000000..7b259a9c5b99d --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx @@ -0,0 +1,129 @@ +/* + * 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 React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { BrowserRouter as Router } from 'react-router-dom'; + +import { + EuiButton, + EuiHorizontalRule, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageHeader, + EuiTitle, + EuiText, +} from '@elastic/eui'; + +import { CoreStart } from '<%= relRoot %>/../src/core/public'; +import { NavigationPublicPluginStart } from '<%= relRoot %>/../src/plugins/navigation/public'; + +import { PLUGIN_ID, PLUGIN_NAME } from '../../common'; + +interface <%= upperCamelCaseName %>AppDeps { + basename: string; + notifications: CoreStart['notifications']; + http: CoreStart['http']; + navigation: NavigationPublicPluginStart; +} + +export const <%= upperCamelCaseName %>App = ({ basename, notifications, http, navigation }: <%= upperCamelCaseName %>AppDeps) => { + // Use React hooks to manage state. + const [timestamp, setTimestamp] = useState(); + + const onClickHandler = () => { +<%_ if (generateApi) { -%> + // Use the core http service to make a response to the server API. + http.get('/api/<%= snakeCase(name) %>/example').then(res => { + setTimestamp(res.time); + // Use the core notifications service to display a success message. + notifications.toasts.addSuccess(i18n.translate('<%= camelCase(name) %>.dataUpdated', { + defaultMessage: 'Data updated', + })); + }); +<%_ } else { -%> + setTimestamp(new Date().toISOString()); + notifications.toasts.addSuccess(PLUGIN_NAME); +<%_ } -%> + }; + + // Render the application DOM. + // Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract. + return ( + + + <> + + + + + +

+ +

+
+
+ + + +

+ +

+
+
+ + +

+ +

+ +

+ +

+ + + +
+
+
+
+
+ +
+
+ ); +}; diff --git a/packages/kbn-plugin-generator/sao_template/template/public/components/main/index.js b/packages/kbn-plugin-generator/sao_template/template/public/components/main/index.js deleted file mode 100644 index 68710baa1bee8..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/components/main/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Main } from './main'; diff --git a/packages/kbn-plugin-generator/sao_template/template/public/components/main/main.js b/packages/kbn-plugin-generator/sao_template/template/public/components/main/main.js deleted file mode 100644 index 59fd667c709aa..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/components/main/main.js +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import { - EuiPage, - EuiPageHeader, - EuiTitle, - EuiPageBody, - EuiPageContent, - EuiPageContentHeader, - EuiPageContentBody, - EuiText -} from '@elastic/eui'; -<%_ if (generateTranslations) { _%> -import { FormattedMessage } from '@kbn/i18n/react'; -<%_ } _%> - -export class Main extends React.Component { - constructor(props) { - super(props); - this.state = {}; - } - - componentDidMount() { - /* - FOR EXAMPLE PURPOSES ONLY. There are much better ways to - manage state and update your UI than this. - */ - const { httpClient } = this.props; - httpClient.get('../api/<%= name %>/example').then((resp) => { - this.setState({ time: resp.data.time }); - }); - } - render() { - const { title } = this.props; - return ( - - - - -

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - {title} Hello World! - <%_ } _%> -

-
-
- - - -

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - Congratulations - <%_ } _%> -

-
-
- - -

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - You have successfully created your first Kibana Plugin! - <%_ } _%> -

-

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - The server time (via API call) is {this.state.time || 'NO API CALL YET'} - <%_ } _%> -

-
-
-
-
-
- ); - } -} diff --git a/packages/kbn-plugin-generator/sao_template/template/public/hack.js b/packages/kbn-plugin-generator/sao_template/template/public/hack.js deleted file mode 100755 index 775526c8e44a3..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/hack.js +++ /dev/null @@ -1,7 +0,0 @@ -import $ from 'jquery'; - -$(document.body).on('keypress', function (event) { - if (event.which === 58) { - alert('boo!'); - } -}); diff --git a/packages/kbn-plugin-generator/sao_template/template/public/app.scss b/packages/kbn-plugin-generator/sao_template/template/public/index.scss similarity index 100% rename from packages/kbn-plugin-generator/sao_template/template/public/app.scss rename to packages/kbn-plugin-generator/sao_template/template/public/index.scss diff --git a/packages/kbn-plugin-generator/sao_template/template/public/index.ts b/packages/kbn-plugin-generator/sao_template/template/public/index.ts new file mode 100644 index 0000000000000..2999dc7264ddb --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/index.ts @@ -0,0 +1,16 @@ +<%_ if (hasScss) { -%> +import './index.scss'; +<%_ } -%> + +import { <%= upperCamelCaseName %>Plugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin() { + return new <%= upperCamelCaseName %>Plugin(); +} +export { + <%= upperCamelCaseName %>PluginSetup, + <%= upperCamelCaseName %>PluginStart, +} from './types'; + diff --git a/packages/kbn-plugin-generator/sao_template/template/public/plugin.ts b/packages/kbn-plugin-generator/sao_template/template/public/plugin.ts new file mode 100644 index 0000000000000..76f7f1a6f9908 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/plugin.ts @@ -0,0 +1,42 @@ +import { i18n } from '@kbn/i18n'; +import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '<%= relRoot %>/src/core/public'; +import { <%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart, AppPluginStartDependencies } from './types'; +import { PLUGIN_NAME } from '../common'; + +export class <%= upperCamelCaseName %>Plugin + implements Plugin<<%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart> { + + public setup(core: CoreSetup): <%= upperCamelCaseName %>PluginSetup { + // Register an application into the side navigation menu + core.application.register({ + id: '<%= camelCase(name) %>', + title: PLUGIN_NAME, + async mount(params: AppMountParameters) { + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services as specified in kibana.json + const [coreStart, depsStart] = await core.getStartServices(); + // Render the application + return renderApp(coreStart, depsStart as AppPluginStartDependencies, params); + }, + }); + + // Return methods that should be available to other plugins + return { + getGreeting() { + return i18n.translate('<%= camelCase(name) %>.greetingText', { + defaultMessage: 'Hello from {name}!', + values: { + name: PLUGIN_NAME, + }, + }); + }, + }; + } + + public start(core: CoreStart): <%= upperCamelCaseName %>PluginStart { + return {}; + } + + public stop() {} +} diff --git a/packages/kbn-plugin-generator/sao_template/template/public/types.ts b/packages/kbn-plugin-generator/sao_template/template/public/types.ts new file mode 100644 index 0000000000000..2ebb0c0d1257f --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/types.ts @@ -0,0 +1,11 @@ +import { NavigationPublicPluginStart } from '<%= relRoot %>/src/plugins/navigation/public'; + +export interface <%= upperCamelCaseName %>PluginSetup { + getGreeting: () => string; +} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface <%= upperCamelCaseName %>PluginStart {} + +export interface AppPluginStartDependencies { + navigation: NavigationPublicPluginStart +}; diff --git a/packages/kbn-plugin-generator/sao_template/template/server/__tests__/index.js b/packages/kbn-plugin-generator/sao_template/template/server/__tests__/index.js deleted file mode 100755 index 9320bd7b028a8..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/server/__tests__/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import expect from '@kbn/expect'; - -describe('suite', () => { - it('is a test', () => { - expect(true).to.equal(true); - }); -}); diff --git a/packages/kbn-plugin-generator/sao_template/template/server/index.ts b/packages/kbn-plugin-generator/sao_template/template/server/index.ts new file mode 100644 index 0000000000000..816b8faec2a45 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/index.ts @@ -0,0 +1,15 @@ +import { PluginInitializerContext } from '<%= relRoot %>/src/core/server'; +import { <%= upperCamelCaseName %>Plugin } from './plugin'; + + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. + + export function plugin(initializerContext: PluginInitializerContext) { + return new <%= upperCamelCaseName %>Plugin(initializerContext); +} + +export { + <%= upperCamelCaseName %>PluginSetup, + <%= upperCamelCaseName %>PluginStart, +} from './types'; diff --git a/packages/kbn-plugin-generator/sao_template/template/server/plugin.ts b/packages/kbn-plugin-generator/sao_template/template/server/plugin.ts new file mode 100644 index 0000000000000..d6a343209e39e --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/plugin.ts @@ -0,0 +1,30 @@ +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '<%= relRoot %>/src/core/server'; + +import { <%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart } from './types'; +import { defineRoutes } from './routes'; + +export class <%= upperCamelCaseName %>Plugin + implements Plugin<<%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart> { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('<%= name %>: Setup'); + const router = core.http.createRouter(); + + // Register server side APIs + defineRoutes(router); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('<%= name %>: Started'); + return {}; + } + + public stop() {} +} diff --git a/packages/kbn-plugin-generator/sao_template/template/server/routes/example.js b/packages/kbn-plugin-generator/sao_template/template/server/routes/example.js deleted file mode 100755 index 5a612645f48fc..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/server/routes/example.js +++ /dev/null @@ -1,11 +0,0 @@ -export default function (server) { - - server.route({ - path: '/api/<%= name %>/example', - method: 'GET', - handler() { - return { time: (new Date()).toISOString() }; - } - }); - -} diff --git a/packages/kbn-plugin-generator/sao_template/template/server/routes/index.ts b/packages/kbn-plugin-generator/sao_template/template/server/routes/index.ts new file mode 100644 index 0000000000000..d8bb00f0dea6c --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/routes/index.ts @@ -0,0 +1,17 @@ +import { IRouter } from '<%= relRoot %>/../src/core/server'; + +export function defineRoutes(router: IRouter) { + router.get( + { + path: '/api/<%= snakeCase(name) %>/example', + validate: false, + }, + async (context, request, response) => { + return response.ok({ + body: { + time: new Date().toISOString(), + }, + }); + } + ); +} diff --git a/packages/kbn-plugin-generator/sao_template/template/server/types.ts b/packages/kbn-plugin-generator/sao_template/template/server/types.ts new file mode 100644 index 0000000000000..adbc5e93f03c5 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/types.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface <%= upperCamelCaseName %>PluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface <%= upperCamelCaseName %>PluginStart {} diff --git a/packages/kbn-plugin-generator/sao_template/template/translations/zh-CN.json b/packages/kbn-plugin-generator/sao_template/template/translations/zh-CN.json deleted file mode 100644 index 3447511c6739a..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/translations/zh-CN.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "formats": { - "number": { - "currency": { - "style": "currency" - }, - "percent": { - "style": "percent" - } - }, - "date": { - "short": { - "month": "numeric", - "day": "numeric", - "year": "2-digit" - }, - "medium": { - "month": "short", - "day": "numeric", - "year": "numeric" - }, - "long": { - "month": "long", - "day": "numeric", - "year": "numeric" - }, - "full": { - "weekday": "long", - "month": "long", - "day": "numeric", - "year": "numeric" - } - }, - "time": { - "short": { - "hour": "numeric", - "minute": "numeric" - }, - "medium": { - "hour": "numeric", - "minute": "numeric", - "second": "numeric" - }, - "long": { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short" - }, - "full": { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short" - } - }, - "relative": { - "years": { - "units": "year" - }, - "months": { - "units": "month" - }, - "days": { - "units": "day" - }, - "hours": { - "units": "hour" - }, - "minutes": { - "units": "minute" - }, - "seconds": { - "units": "second" - } - } - }, - "messages": { - "<%= camelCase(name) %>.congratulationsText": "您已经成功创建第一个 Kibana 插件。", - "<%= camelCase(name) %>.congratulationsTitle": "恭喜!", - "<%= camelCase(name) %>.helloWorldText": "{title} 您好,世界!", - "<%= camelCase(name) %>.serverTimeText": "服务器时间(通过 API 调用)为 {time}" - } -} diff --git a/packages/kbn-plugin-generator/tsconfig.json b/packages/kbn-plugin-generator/tsconfig.json new file mode 100644 index 0000000000000..fe0f7112f1fa9 --- /dev/null +++ b/packages/kbn-plugin-generator/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["**/*", "index.js.d.ts"], + "exclude": ["sao_template/template/*"] +} diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 47a47449927e4..ea15d2e34ea3a 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --watch" }, "devDependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "@elastic/charts": "^16.1.0", "@kbn/dev-utils": "1.0.0", "@kbn/i18n": "1.0.0", diff --git a/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap index 0ebc44ba67862..cf3b48f237286 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap @@ -25,6 +25,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` Object { "baseUrl": "http://localhost:5601/app/siem", "category": Object { + "euiIconType": "logoSecurity", "label": "Security", "order": 3000, }, @@ -36,6 +37,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` Object { "baseUrl": "http://localhost:5601/app/metrics", "category": Object { + "euiIconType": "logoObservability", "label": "Observability", "order": 2000, }, @@ -90,6 +92,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` Object { "baseUrl": "http://localhost:5601/app/logs", "category": Object { + "euiIconType": "logoObservability", "label": "Observability", "order": 2000, }, @@ -120,6 +123,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` }, Object { "category": Object { + "euiIconType": "logoSecurity", "label": "Security", "order": 3000, }, @@ -136,6 +140,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` }, Object { "category": Object { + "euiIconType": "logoObservability", "label": "Observability", "order": 2000, }, @@ -215,6 +220,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` }, Object { "category": Object { + "euiIconType": "logoObservability", "label": "Observability", "order": 2000, }, @@ -390,7 +396,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` "listItems": Array [], "title": "Recent items", }, - "iconType": "clock", + "iconType": "recentlyViewedApp", "isDisabled": true, "label": "Recently viewed", }, @@ -408,7 +414,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` "aria-label": "Recently viewed", "className": "euiNavDrawerGroup__item", "data-name": "Recently viewed", - "iconType": "clock", + "iconType": "recentlyViewedApp", "isDisabled": true, "label": "Recently viewed", "onClick": [Function], @@ -427,7 +433,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` aria-label="Recently viewed" className="euiNavDrawerGroup__item" data-name="Recently viewed" - iconType="clock" + iconType="recentlyViewedApp" isDisabled={true} key="title-0" label="Recently viewed" @@ -463,18 +469,18 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` > - O - , - "iconType": undefined, + "iconType": "logoObservability", "label": "Observability", "onClick": [Function], "size": "s", @@ -689,6 +693,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` Object { "aria-label": "Security", "category": Object { + "euiIconType": "logoSecurity", "label": "Security", "order": 3000, }, @@ -696,12 +701,8 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` "data-name": "Security", "data-test-subj": "navDrawerAppsMenuLink", "href": "http://localhost:5601/app/siem", - "icon": - S - , - "iconType": undefined, + "icon": undefined, + "iconType": "logoSecurity", "isActive": false, "isDisabled": undefined, "key": "siem", @@ -809,13 +810,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` className="euiNavDrawerGroup__item" data-name="Observability" data-test-subj="navDrawerCategory" - icon={ - - O - - } + iconType="logoObservability" key="title-1" label="Observability" onClick={[Function]} @@ -849,11 +844,30 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` onFocus={[Function]} type="button" > - - O - + + + + @@ -868,6 +882,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` aria-label="Security" category={ Object { + "euiIconType": "logoSecurity", "label": "Security", "order": 3000, } @@ -876,13 +891,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` data-name="Security" data-test-subj="navDrawerAppsMenuLink" href="http://localhost:5601/app/siem" - icon={ - - S - - } + iconType="logoSecurity" isActive={false} key="siem" label="Security" @@ -910,6 +919,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` aria-label="Security" category={ Object { + "euiIconType": "logoSecurity", "label": "Security", "order": 3000, } @@ -923,11 +933,30 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` onFocus={[Function]} order={500} > - - S - + + + + @@ -1222,6 +1251,7 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t Object { "baseUrl": "http://localhost:5601/app/siem", "category": Object { + "euiIconType": "logoSecurity", "label": "Security", "order": 3000, }, @@ -1233,6 +1263,7 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t Object { "baseUrl": "http://localhost:5601/app/metrics", "category": Object { + "euiIconType": "logoObservability", "label": "Observability", "order": 2000, }, @@ -1285,6 +1316,7 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t }, Object { "category": Object { + "euiIconType": "logoSecurity", "label": "Security", "order": 3000, }, @@ -1301,6 +1333,7 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t }, Object { "category": Object { + "euiIconType": "logoObservability", "label": "Observability", "order": 2000, }, @@ -1508,7 +1541,7 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t "listItems": Array [], "title": "Recent items", }, - "iconType": "clock", + "iconType": "recentlyViewedApp", "isDisabled": true, "label": "Recently viewed", }, @@ -1526,7 +1559,7 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t "aria-label": "Recently viewed", "className": "euiNavDrawerGroup__item", "data-name": "Recently viewed", - "iconType": "clock", + "iconType": "recentlyViewedApp", "isDisabled": true, "label": "Recently viewed", "onClick": [Function], @@ -1545,7 +1578,7 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t aria-label="Recently viewed" className="euiNavDrawerGroup__item" data-name="Recently viewed" - iconType="clock" + iconType="recentlyViewedApp" isDisabled={true} key="title-0" label="Recently viewed" @@ -1581,18 +1614,18 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t > { const fullOldPath = getPath(rootPath, oldKey); const oldValue = get(config, fullOldPath); @@ -40,11 +41,16 @@ const _rename = ( const newValue = get(config, fullNewPath); if (newValue === undefined) { set(config, fullNewPath, oldValue); - log(`"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}"`); + + if (!silent) { + log(`"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}"`); + } } else { - log( - `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}". However both key are present, ignoring "${fullOldPath}"` - ); + if (!silent) { + log( + `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}". However both key are present, ignoring "${fullOldPath}"` + ); + } } return config; }; @@ -67,11 +73,11 @@ const _unused = ( const rename = (oldKey: string, newKey: string): ConfigDeprecation => (config, rootPath, log) => _rename(config, rootPath, log, oldKey, newKey); -const renameFromRoot = (oldKey: string, newKey: string): ConfigDeprecation => ( +const renameFromRoot = (oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation => ( config, rootPath, log -) => _rename(config, '', log, oldKey, newKey); +) => _rename(config, '', log, oldKey, newKey, silent); const unused = (unusedKey: string): ConfigDeprecation => (config, rootPath, log) => _unused(config, rootPath, log, unusedKey); diff --git a/src/core/server/config/deprecation/types.ts b/src/core/server/config/deprecation/types.ts index 19fba7800c919..dbfbad771f074 100644 --- a/src/core/server/config/deprecation/types.ts +++ b/src/core/server/config/deprecation/types.ts @@ -102,7 +102,7 @@ export interface ConfigDeprecationFactory { * ] * ``` */ - renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; + renameFromRoot(oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation; /** * Remove a configuration property from inside a plugin's configuration path. * Will log a deprecation warning if the unused key was found and deprecation applied. diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index f8ef49b0f6d18..a9fc80c86d878 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -62,6 +62,7 @@ beforeAll(() => { beforeEach(() => { config = { + name: 'kibana', host: '127.0.0.1', maxPayload: new ByteSizeValue(1024), port: 10002, @@ -1077,4 +1078,37 @@ describe('setup contract', () => { expect(isTlsEnabled).toBe(false); }); }); + + describe('#getServerInfo', () => { + it('returns correct information', async () => { + let { getServerInfo } = await server.setup(config); + + expect(getServerInfo()).toEqual({ + host: '127.0.0.1', + name: 'kibana', + port: 10002, + protocol: 'http', + }); + + ({ getServerInfo } = await server.setup({ + ...config, + port: 12345, + name: 'custom-name', + host: 'localhost', + })); + + expect(getServerInfo()).toEqual({ + host: 'localhost', + name: 'custom-name', + port: 12345, + protocol: 'http', + }); + }); + + it('returns correct protocol when ssl is enabled', async () => { + const { getServerInfo } = await server.setup(configWithSSL); + + expect(getServerInfo().protocol).toEqual('https'); + }); + }); }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index fdc272041ce35..025ab2bf56ac2 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -35,7 +35,7 @@ import { import { IsAuthenticated, AuthStateStorage, GetAuthState } from './auth_state_storage'; import { AuthHeadersStorage, GetAuthHeaders } from './auth_headers_storage'; import { BasePath } from './base_path_service'; -import { HttpServiceSetup } from './types'; +import { HttpServiceSetup, HttpServerInfo } from './types'; /** @internal */ export interface HttpServerSetup { @@ -58,6 +58,7 @@ export interface HttpServerSetup { get: GetAuthState; isAuthenticated: IsAuthenticated; }; + getServerInfo: () => HttpServerInfo; } /** @internal */ @@ -122,6 +123,12 @@ export class HttpServer { isAuthenticated: this.authState.isAuthenticated, }, getAuthHeaders: this.authRequestHeaders.get, + getServerInfo: () => ({ + name: config.name, + host: config.host, + port: config.port, + protocol: this.server!.info.protocol, + }), isTlsEnabled: config.ssl.enabled, // Return server instance with the connection options so that we can properly // bridge core and the "legacy" Kibana internally. Once this bridge isn't diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 2b2d98d937e85..30032ff5da796 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -77,12 +77,19 @@ const createSetupContractMock = () => { auth: createAuthMock(), getAuthHeaders: jest.fn(), isTlsEnabled: false, + getServerInfo: jest.fn(), }; setupContract.createCookieSessionStorageFactory.mockResolvedValue( sessionStorageMock.createFactory() ); setupContract.createRouter.mockImplementation(() => mockRouter.create()); setupContract.getAuthHeaders.mockReturnValue({ authorization: 'authorization-header' }); + setupContract.getServerInfo.mockReturnValue({ + host: 'localhost', + name: 'kibana', + port: 80, + protocol: 'http', + }); return setupContract; }; diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts index 01b852c26ec93..6327844108055 100644 --- a/src/core/server/http/types.ts +++ b/src/core/server/http/types.ts @@ -252,6 +252,11 @@ export interface HttpServiceSetup { contextName: T, provider: RequestHandlerContextProvider ) => RequestHandlerContextContainer; + + /** + * Provides common {@link HttpServerInfo | information} about the running http server. + */ + getServerInfo: () => HttpServerInfo; } /** @internal */ @@ -273,3 +278,15 @@ export interface HttpServiceStart { /** Indicates if http server is listening on a given port */ isListening: (port: number) => boolean; } + +/** @public */ +export interface HttpServerInfo { + /** The name of the Kibana server */ + name: string; + /** The hostname of the server */ + host: string; + /** The port the server is listening on */ + port: number; + /** The protocol used by the server */ + protocol: 'http' | 'https' | 'socket'; +} diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 6477b2d51d300..508b3e66469e3 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -103,6 +103,7 @@ export { GetAuthState, HttpResponseOptions, HttpResponsePayload, + HttpServerInfo, HttpServiceSetup, HttpServiceStart, ErrorHttpResponseOptions, diff --git a/src/core/server/kibana_config.ts b/src/core/server/kibana_config.ts index d46960289a8d0..17f77a6e9328f 100644 --- a/src/core/server/kibana_config.ts +++ b/src/core/server/kibana_config.ts @@ -25,9 +25,7 @@ export const config = { path: 'kibana', schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), - defaultAppId: schema.string({ defaultValue: 'home' }), index: schema.string({ defaultValue: '.kibana' }), - disableWelcomeScreen: schema.boolean({ defaultValue: false }), autocompleteTerminateAfter: schema.duration({ defaultValue: 100000 }), autocompleteTimeout: schema.duration({ defaultValue: 1000 }), }), diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 1f725ee945218..b2501496d87ef 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -293,6 +293,7 @@ export class LegacyService implements CoreService { }, csp: setupDeps.core.http.csp, isTlsEnabled: setupDeps.core.http.isTlsEnabled, + getServerInfo: setupDeps.core.http.getServerInfo, }, savedObjects: { setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 566fac4051c5d..1dfb294ff8b38 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -44,7 +44,7 @@ import { uuidServiceMock } from './uuid/uuid_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { - kibana: { defaultAppId: 'home-mocks', index: '.kibana-tests' }, + kibana: { index: '.kibana-tests' }, elasticsearch: { shardTimeout: duration('30s'), requestTimeout: duration('30s'), @@ -106,6 +106,7 @@ function createCoreSetupMock() { get: httpService.auth.get, isAuthenticated: httpService.auth.isAuthenticated, }, + getServerInfo: httpService.getServerInfo, }; httpMock.createRouter.mockImplementation(() => httpService.createRouter('')); diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index 3fcd7fbbbe1ff..823299771544c 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -75,7 +75,7 @@ describe('Plugin Context', () => { .pipe(first()) .toPromise(); expect(configObject).toStrictEqual({ - kibana: { defaultAppId: 'home', index: '.kibana' }, + kibana: { index: '.kibana' }, elasticsearch: { shardTimeout: duration(30, 's'), requestTimeout: duration(30, 's'), diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index c35fa25dfe8cb..a7b555a9eba01 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -164,6 +164,7 @@ export function createPluginSetupContext( auth: { get: deps.http.auth.get, isAuthenticated: deps.http.auth.isAuthenticated }, csp: deps.http.csp, isTlsEnabled: deps.http.isTlsEnabled, + getServerInfo: deps.http.getServerInfo, }, savedObjects: { setClientFactoryProvider: deps.savedObjects.setClientFactoryProvider, diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index a89e2f8c684e4..9ae04787767bb 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -214,7 +214,7 @@ export interface Plugin< export const SharedGlobalConfigKeys = { // We can add more if really needed - kibana: ['defaultAppId', 'index'] as const, + kibana: ['index'] as const, elasticsearch: ['shardTimeout', 'requestTimeout', 'pingTimeout', 'startupTimeout'] as const, path: ['data'] as const, }; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index a2806cec034f7..4b0015fb8c163 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -509,7 +509,7 @@ export type ConfigDeprecation = (config: Record, fromPath: string, // @public export interface ConfigDeprecationFactory { rename(oldKey: string, newKey: string): ConfigDeprecation; - renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; + renameFromRoot(oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation; unused(unusedKey: string): ConfigDeprecation; unusedFromRoot(unusedKey: string): ConfigDeprecation; } @@ -736,6 +736,14 @@ export interface HttpResponseOptions { // @public export type HttpResponsePayload = undefined | string | Record | Buffer | Stream; +// @public (undocumented) +export interface HttpServerInfo { + host: string; + name: string; + port: number; + protocol: 'http' | 'https' | 'socket'; +} + // @public export interface HttpServiceSetup { // (undocumented) @@ -747,6 +755,7 @@ export interface HttpServiceSetup { createCookieSessionStorageFactory: (cookieOptions: SessionStorageCookieOptions) => Promise>; createRouter: () => IRouter; csp: ICspConfig; + getServerInfo: () => HttpServerInfo; isTlsEnabled: boolean; registerAuth: (handler: AuthenticationHandler) => void; registerOnPostAuth: (handler: OnPostAuthHandler) => void; diff --git a/src/core/utils/default_app_categories.ts b/src/core/utils/default_app_categories.ts index 3e3cc2fef2a22..2285bd6afd365 100644 --- a/src/core/utils/default_app_categories.ts +++ b/src/core/utils/default_app_categories.ts @@ -31,6 +31,7 @@ export const DEFAULT_APP_CATEGORIES = Object.freeze({ label: i18n.translate('core.ui.observabilityNavList.label', { defaultMessage: 'Observability', }), + euiIconType: 'logoObservability', order: 2000, }, security: { @@ -38,6 +39,7 @@ export const DEFAULT_APP_CATEGORIES = Object.freeze({ defaultMessage: 'Security', }), order: 3000, + euiIconType: 'logoSecurity', }, management: { label: i18n.translate('core.ui.managementNavList.label', { diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 8e0497732e230..8c35044b52c9e 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -42,9 +42,7 @@ export default function(kibana) { config: function(Joi) { return Joi.object({ enabled: Joi.boolean().default(true), - defaultAppId: Joi.string().default('home'), index: Joi.string().default('.kibana'), - disableWelcomeScreen: Joi.boolean().default(false), autocompleteTerminateAfter: Joi.number() .integer() .min(1) diff --git a/src/legacy/core_plugins/kibana/inject_vars.js b/src/legacy/core_plugins/kibana/inject_vars.js index 4bf11f28732ee..01623341e4d38 100644 --- a/src/legacy/core_plugins/kibana/inject_vars.js +++ b/src/legacy/core_plugins/kibana/inject_vars.js @@ -28,8 +28,6 @@ export function injectVars(server) { ); return { - kbnDefaultAppId: serverConfig.get('kibana.defaultAppId'), - disableWelcomeScreen: serverConfig.get('kibana.disableWelcomeScreen'), importAndExportableTypes, autocompleteTerminateAfter: serverConfig.get('kibana.autocompleteTerminateAfter'), autocompleteTimeout: serverConfig.get('kibana.autocompleteTimeout'), diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index 0d461028d994a..f1fd93fd09b3d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -46,6 +46,7 @@ import { IEmbeddableStart } from '../../../../../../plugins/embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../../plugins/navigation/public'; import { DataPublicPluginStart as NpDataStart } from '../../../../../../plugins/data/public'; import { SharePluginStart } from '../../../../../../plugins/share/public'; +import { KibanaLegacyStart } from '../../../../../../plugins/kibana_legacy/public'; export interface RenderDeps { core: LegacyCoreStart; @@ -62,6 +63,7 @@ export interface RenderDeps { embeddables: IEmbeddableStart; localStorage: Storage; share: SharePluginStart; + config: KibanaLegacyStart['config']; } let angularModuleInstance: IModule | null = null; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx index a48c165116304..0537e3f8fc456 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx @@ -88,7 +88,6 @@ export interface DashboardAppScope extends ng.IScope { export function initDashboardAppDirective(app: any, deps: RenderDeps) { app.directive('dashboardApp', function($injector: IInjector) { const confirmModal = $injector.get('confirmModal'); - const config = deps.uiSettings; return { restrict: 'E', @@ -106,7 +105,6 @@ export function initDashboardAppDirective(app: any, deps: RenderDeps) { $route, $scope, $routeParams, - config, confirmModal, indexPatterns: deps.npDataStart.indexPatterns, kbnUrlStateStorage, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index a8eec9c2504a7..624be02ac3b9d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -90,7 +90,6 @@ export interface DashboardAppControllerDependencies extends RenderDeps { $routeParams: any; indexPatterns: IndexPatternsContract; dashboardConfig: any; - config: any; confirmModal: ConfirmModalFn; history: History; kbnUrlStateStorage: IKbnUrlStateStorage; @@ -109,7 +108,6 @@ export class DashboardAppController { dashboardConfig, localStorage, indexPatterns, - config, confirmModal, savedQueryService, embeddables, @@ -376,7 +374,7 @@ export class DashboardAppController { dashboardStateManager.getQuery() || { query: '', language: - localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage'), + localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), }, queryFilter.getFilters() ); @@ -493,7 +491,7 @@ export class DashboardAppController { { query: '', language: - localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage'), + localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), }, queryFilter.getGlobalFilters() ); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index abc0c789326f8..b0f70b7a0c68f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -246,10 +246,10 @@ export function initDashboardApp(app, deps) { }, }) .when(`dashboard/:tail*?`, { - redirectTo: `/${deps.core.injectedMetadata.getInjectedVar('kbnDefaultAppId')}`, + redirectTo: `/${deps.config.defaultAppId}`, }) .when(`dashboards/:tail*?`, { - redirectTo: `/${deps.core.injectedMetadata.getInjectedVar('kbnDefaultAppId')}`, + redirectTo: `/${deps.config.defaultAppId}`, }); }); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap index e76f65c45e428..f5a00e5435ed6 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap @@ -5,7 +5,6 @@ exports[`renders DashboardCloneModal 1`] = ` diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index ca4b18a37504c..227bcb53ca0df 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -107,6 +107,7 @@ export class DashboardPlugin implements Plugin { chrome: contextCore.chrome, addBasePath: contextCore.http.basePath.prepend, uiSettings: contextCore.uiSettings, + config: kibana_legacy.config, savedQueryService: npDataStart.query.savedQueries, embeddables, dashboardCapabilities: contextCore.application.capabilities.dashboard, diff --git a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts index 4d9177735556d..90fb32a88d43c 100644 --- a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts @@ -29,7 +29,12 @@ import { UiSettingsState, } from 'kibana/public'; import { UiStatsMetricType } from '@kbn/analytics'; -import { Environment, FeatureCatalogueEntry } from '../../../../../plugins/home/public'; +import { + Environment, + FeatureCatalogueEntry, + HomePublicPluginSetup, +} from '../../../../../plugins/home/public'; +import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; export interface HomeKibanaServices { indexPatternService: any; @@ -51,6 +56,8 @@ export interface HomeKibanaServices { chrome: ChromeStart; telemetryOptInProvider: any; uiSettings: IUiSettingsClient; + config: KibanaLegacySetup['config']; + homeConfig: HomePublicPluginSetup['config']; http: HttpStart; savedObjectsClient: SavedObjectsClientContract; toastNotifications: NotificationsSetup['toasts']; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/add_data.test.js.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/add_data.test.js.snap index 35a7216df8bc6..fcc7e75276154 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/add_data.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/add_data.test.js.snap @@ -4,143 +4,190 @@ exports[`apmUiEnabled 1`] = ` - - - -

- -

- - -

- -

-
- - - - - APM automatically collects in-depth performance metrics and errors from inside your applications. -
- } - footer={ - - - - } - icon={ + + + - } - title="APM" - /> - - - - Ingest logs from popular data sources and easily visualize in preconfigured dashboards. -
- } - footer={ - + + - - - } - icon={ - + + + + + + + + + + APM automatically collects in-depth performance metrics and errors from inside your applications. + + } + footer={ + + + + } + textAlign="left" + title="APM" + titleSize="xs" /> - } - title="Logs" - /> + + + + Ingest logs from popular data sources and easily visualize in preconfigured dashboards. + + } + footer={ + + + + } + textAlign="left" + title="Logs" + titleSize="xs" + /> + + + + Collect metrics from the operating system and services running on your servers. + + } + footer={ + + + + } + textAlign="left" + title="Metrics" + titleSize="xs" + /> + + - - Collect metrics from the operating system and services running on your servers. - - } - footer={ - - - - } - icon={ + + - } - title="Metrics" + + + +

+ +

+
+
+
+ -
- } - icon={ - - } + textAlign="left" title="SIEM" + titleSize="xs" /> @@ -257,109 +300,159 @@ exports[`isNewKibanaInstance 1`] = ` - - - -

- -

-
- -

- -

-
-
-
- - - Ingest logs from popular data sources and easily visualize in preconfigured dashboards. - - } - footer={ - - - - } - icon={ + + + - } - title="Logs" - /> + + + +

+ +

+
+
+
+ + + + + Ingest logs from popular data sources and easily visualize in preconfigured dashboards. + + } + footer={ + + + + } + textAlign="left" + title="Logs" + titleSize="xs" + /> + + + + Collect metrics from the operating system and services running on your servers. + + } + footer={ + + + + } + textAlign="left" + title="Metrics" + titleSize="xs" + /> + +
- - Collect metrics from the operating system and services running on your servers. - - } - footer={ - - - - } - icon={ + + - } - title="Metrics" + + + +

+ +

+
+
+
+ -
- } - icon={ - - } + textAlign="left" title="SIEM" + titleSize="xs" />
@@ -476,143 +565,190 @@ exports[`mlEnabled 1`] = ` - - - -

- -

-
- -

- -

-
-
-
- - - APM automatically collects in-depth performance metrics and errors from inside your applications. - - } - footer={ - - - - } - icon={ + + + - } - title="APM" - /> - - - - Ingest logs from popular data sources and easily visualize in preconfigured dashboards. - - } - footer={ - + + - - - } - icon={ - + + + + + + + + + + APM automatically collects in-depth performance metrics and errors from inside your applications. + + } + footer={ + + + + } + textAlign="left" + title="APM" + titleSize="xs" /> - } - title="Logs" - /> + + + + Ingest logs from popular data sources and easily visualize in preconfigured dashboards. + + } + footer={ + + + + } + textAlign="left" + title="Logs" + titleSize="xs" + /> + + + + Collect metrics from the operating system and services running on your servers. + + } + footer={ + + + + } + textAlign="left" + title="Metrics" + titleSize="xs" + /> + + - - Collect metrics from the operating system and services running on your servers. - - } - footer={ - - - - } - icon={ + + - } - title="Metrics" + + + +

+ +

+
+
+
+ -
- } - icon={ - - } + textAlign="left" title="SIEM" + titleSize="xs" />
@@ -765,109 +897,159 @@ exports[`render 1`] = ` - - - -

- -

-
- -

- -

-
-
-
- - - Ingest logs from popular data sources and easily visualize in preconfigured dashboards. - - } - footer={ - - - - } - icon={ + + + - } - title="Logs" - /> + + + +

+ +

+
+
+
+ + + + + Ingest logs from popular data sources and easily visualize in preconfigured dashboards. + + } + footer={ + + + + } + textAlign="left" + title="Logs" + titleSize="xs" + /> + + + + Collect metrics from the operating system and services running on your servers. + + } + footer={ + + + + } + textAlign="left" + title="Metrics" + titleSize="xs" + /> + +
- - Collect metrics from the operating system and services running on your servers. - - } - footer={ - - - - } - icon={ + + - } - title="Metrics" + + + +

+ +

+
+
+
+ -
- } - icon={ - - } + textAlign="left" title="SIEM" + titleSize="xs" />
diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/synopsis.test.js.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/synopsis.test.js.snap index 34cc0eb9265ff..525cc5bdda9d4 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/synopsis.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/synopsis.test.js.snap @@ -15,6 +15,7 @@ exports[`props iconType 1`] = ` } layout="horizontal" title="Great tutorial" + titleSize="xs" /> `; @@ -34,6 +35,7 @@ exports[`props iconUrl 1`] = ` } layout="horizontal" title="Great tutorial" + titleSize="xs" /> `; @@ -46,6 +48,7 @@ exports[`props isBeta 1`] = ` href="link_to_item" layout="horizontal" title="Great tutorial" + titleSize="xs" /> `; @@ -58,5 +61,6 @@ exports[`render 1`] = ` href="link_to_item" layout="horizontal" title="Great tutorial" + titleSize="xs" /> `; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/_add_data.scss b/src/legacy/core_plugins/kibana/public/home/np_ready/components/_add_data.scss index 2b87ddbcf4673..836b34227a37c 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/_add_data.scss +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/_add_data.scss @@ -1,9 +1,22 @@ .homAddData__card { - max-width: 250px; border: none; box-shadow: none; } +.homAddData__cardDivider { + position: relative; + + &:after { + position: absolute; + content: ''; + width: 1px; + right: -$euiSizeS; + top: 0; + bottom: 0; + background: $euiBorderColor; + } +} + .homAddData__icon { width: $euiSizeXL * 2; height: $euiSizeXL * 2; @@ -18,10 +31,8 @@ text-align: center; } -@include euiBreakpoint('xs') { - .homAddData__card { - max-width: none; - } +.homAddData__logo { + margin-left: $euiSize; } @include euiBreakpoint('xs', 's') { @@ -30,6 +41,21 @@ } } +@include euiBreakpoint('xs', 's', 'm') { + .homAddDat__flexTablet { + flex-direction: column; + } + + .homAddData__cardDivider:after { + display: none; + } + + .homAddData__cardDivider { + flex-grow: 0 !important; + flex-basis: 100% !important; + } +} + @include euiBreakpoint('l', 'xl') { .homeAddData__flexGroup { flex-wrap: nowrap; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/add_data.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/add_data.js index a49620be2d229..6d980114f9647 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/add_data.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/add_data.js @@ -91,10 +91,11 @@ const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => { }; const getApmCard = () => ( - + } + titleSize="xs" title={apmData.title} description={{apmData.description}} footer={ @@ -115,60 +116,110 @@ const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => { return ( - {apmUiEnabled !== false && getApmCard()} + + + + + + + + +

+ +

+
+
+
+ + + {apmUiEnabled !== false && getApmCard()} - - } - title={loggingData.title} - description={{loggingData.description}} - footer={ - - - - } - /> - + + {loggingData.description} + } + footer={ + + + + } + /> + - - } - title={metricsData.title} - description={{metricsData.description}} - footer={ - - - - } - /> + + {metricsData.description} + } + footer={ + + + + } + /> + +
- + + + + + + + +

+ +

+
+
+
+ } title={siemData.title} description={{siemData.description}} footer={ @@ -179,7 +230,7 @@ const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => { > } @@ -195,29 +246,6 @@ const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => { return ( - - - -

- -

-
- -

- -

-
-
-
- - - {renderCards()} diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js index 5c32a463da115..0c09c6c3c74fc 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js @@ -48,7 +48,7 @@ export class Home extends Component { super(props); const isWelcomeEnabled = !( - getServices().getInjected('disableWelcomeScreen') || + getServices().homeConfig.disableWelcomeScreen || props.localStorage.getItem(KEY_ENABLE_WELCOME) === 'false' ); const currentOptInStatus = this.props.getOptInStatus(); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js index 27d4f1a8b1c1f..d25a1f81dae5a 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js @@ -30,6 +30,7 @@ jest.mock('../../kibana_services', () => ({ getServices: () => ({ getBasePath: () => 'path', getInjected: () => '', + homeConfig: { disableWelcomeScreen: false }, }), })); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js index e49f00b949da5..f6c91b412381c 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js @@ -30,7 +30,7 @@ import { replaceTemplateStrings } from './tutorial/replace_template_strings'; import { getServices } from '../../kibana_services'; export function HomeApp({ directories }) { const { - getInjected, + config, savedObjectsClient, getBasePath, addBasePath, @@ -41,7 +41,7 @@ export function HomeApp({ directories }) { const mlEnabled = environment.ml; const apmUiEnabled = environment.apmUi; - const defaultAppId = getInjected('kbnDefaultAppId', 'discover'); + const defaultAppId = config.defaultAppId || 'discover'; const renderTutorialDirectory = props => { return ( diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js index 38dff429f6d35..968b8eb64def5 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js @@ -55,6 +55,7 @@ export function Synopsis({ className={classes} layout="horizontal" icon={optionalImg} + titleSize="xs" title={title} description={description} onClick={onClick} diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/__snapshots__/instruction_set.test.js.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/__snapshots__/instruction_set.test.js.snap index 0d962ac514137..9409963ff95aa 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/__snapshots__/instruction_set.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/__snapshots__/instruction_set.test.js.snap @@ -22,11 +22,7 @@ exports[`render 1`] = ` grow={false} />
- + - + - + - + - + - + - <path d="M6.5 12a.502.502 0 01-.354-.146l-4-4a.502.502 0 01.708-.708L6.5 10.793l6.646-6.647a.502.502 0 01.708.708l-7 7A.502.502 0 016.5 12" fillRule="evenodd" diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts index 502c8f45646cf..aec3835dc075d 100644 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts @@ -27,6 +27,7 @@ import { Environment, FeatureCatalogueEntry, HomePublicPluginStart, + HomePublicPluginSetup, } from '../../../../../plugins/home/public'; export interface LegacyAngularInjectedDependencies { @@ -59,6 +60,7 @@ export interface HomePluginSetupDependencies { }; usageCollection: UsageCollectionSetup; kibana_legacy: KibanaLegacySetup; + home: HomePublicPluginSetup; } export class HomePlugin implements Plugin { @@ -69,6 +71,7 @@ export class HomePlugin implements Plugin { setup( core: CoreSetup, { + home, kibana_legacy, usageCollection, __LEGACY: { getAngularDependencies, ...legacyServices }, @@ -95,6 +98,8 @@ export class HomePlugin implements Plugin { getBasePath: core.http.basePath.get, indexPatternService: this.dataStart!.indexPatterns, environment: this.environment!, + config: kibana_legacy.config, + homeConfig: home.config, ...angularDependencies, }); const { renderApp } = await import('./np_ready/application'); diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 50f1702a2a6d0..f2868da947a75 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -20,7 +20,6 @@ // autoloading // preloading (for faster webpack builds) -import chrome from 'ui/chrome'; import routes from 'ui/routes'; import { uiModules } from 'ui/modules'; import { npSetup } from 'ui/new_platform'; @@ -64,8 +63,9 @@ localApplicationService.attachToAngular(routes); routes.enable(); +const { config } = npSetup.plugins.kibana_legacy; routes.otherwise({ - redirectTo: `/${chrome.getInjected('kbnDefaultAppId', 'discover')}`, + redirectTo: `/${config.defaultAppId || 'discover'}`, }); uiModules.get('kibana').run(showAppRedirectNotification); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap index 4ba0fe480ac42..2c0a5d8f6b8f1 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap @@ -78,7 +78,6 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = ` exports[`ObjectsTable export should allow the user to choose when exporting all 1`] = ` <EuiModal - maxWidth={true} onClose={[Function]} > <EuiModalHeader> diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index f7fd19e8288e7..15e9c73a39eff 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -34,6 +34,7 @@ import { VisualizationsStart } from '../../../visualizations/public'; import { SavedVisualizations } from './np_ready/types'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; import { Chrome } from './legacy_imports'; +import { KibanaLegacyStart } from '../../../../../plugins/kibana_legacy/public'; export interface VisualizeKibanaServices { addBasePath: (url: string) => string; @@ -52,6 +53,7 @@ export interface VisualizeKibanaServices { savedVisualizations: SavedVisualizations; share: SharePluginStart; uiSettings: IUiSettingsClient; + config: KibanaLegacyStart['config']; visualizeCapabilities: any; visualizations: VisualizationsStart; usageCollection?: UsageCollectionSetup; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js index d99771ccc912d..24055b9a2d9ed 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js @@ -173,7 +173,7 @@ export function initVisualizeApp(app, deps) { }, }) .when(`visualize/:tail*?`, { - redirectTo: `/${deps.core.injectedMetadata.getInjectedVar('kbnDefaultAppId')}`, + redirectTo: `/${deps.config.defaultAppId}`, }); }); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/__snapshots__/new_vis_modal.test.tsx.snap index c75fd2096feab..2dcf3ed0ea06f 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -442,7 +442,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1` <EuiModal aria-label="Start creating your visualization by selecting a type for that visualization. Hit escape to close this modal. Hit Tab key to go further." className="visNewVisDialog" - maxWidth={true} onClose={[Function]} role="menu" > @@ -2179,7 +2178,6 @@ exports[`NewVisModal should render as expected 1`] = ` <EuiModal aria-label="Start creating your visualization by selecting a type for that visualization. Hit escape to close this modal. Hit Tab key to go further." className="visNewVisDialog" - maxWidth={true} onClose={[Function]} role="menu" > @@ -2897,7 +2895,6 @@ exports[`NewVisModal should render as expected 1`] = ` value="" > <EuiFormControlLayout - clear={null} compressed={false} fullWidth={true} icon="search" @@ -2922,7 +2919,6 @@ exports[`NewVisModal should render as expected 1`] = ` /> </EuiValidatableControl> <EuiFormControlLayoutIcons - clear={null} icon="search" isLoading={false} > diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 26c6691a3613f..8e7487fee55f6 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -108,6 +108,7 @@ export class VisualizePlugin implements Plugin { share, toastNotifications: contextCore.notifications.toasts, uiSettings: contextCore.uiSettings, + config: kibana_legacy.config, visualizeCapabilities: contextCore.application.capabilities.visualize, visualizations, usageCollection, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts index 9bf7ee3d59401..056849a292657 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts @@ -39,6 +39,7 @@ import { createGoalVisTypeDefinition, } from './vis_type_vislib_vis_types'; import { ChartsPluginSetup } from '../../../../plugins/charts/public'; +import { ConfigSchema as VisTypeXyConfigSchema } from '../../vis_type_xy'; export interface VisTypeVislibDependencies { uiSettings: IUiSettingsClient; @@ -72,11 +73,7 @@ export class VisTypeVislibPlugin implements Plugin<Promise<void>, void> { uiSettings: core.uiSettings, charts, }; - - expressions.registerFunction(createVisTypeVislibVisFn); - expressions.registerFunction(createPieVisFn); - - [ + const vislibTypes = [ createHistogramVisTypeDefinition, createLineVisTypeDefinition, createPieVisTypeDefinition, @@ -85,7 +82,30 @@ export class VisTypeVislibPlugin implements Plugin<Promise<void>, void> { createHorizontalBarVisTypeDefinition, createGaugeVisTypeDefinition, createGoalVisTypeDefinition, - ].forEach(vis => visualizations.types.createBaseVisualization(vis(visualizationDependencies))); + ]; + const vislibFns = [createVisTypeVislibVisFn, createPieVisFn]; + + const visTypeXy = core.injectedMetadata.getInjectedVar('visTypeXy') as + | VisTypeXyConfigSchema['visTypeXy'] + | undefined; + + // if visTypeXy plugin is disabled it's config will be undefined + if (!visTypeXy || !visTypeXy.enabled) { + const convertedTypes: any[] = []; + const convertedFns: any[] = []; + + // Register legacy vislib types that have been converted + convertedFns.forEach(expressions.registerFunction); + convertedTypes.forEach(vis => + visualizations.types.createBaseVisualization(vis(visualizationDependencies)) + ); + } + + // Register non-converted types + vislibFns.forEach(expressions.registerFunction); + vislibTypes.forEach(vis => + visualizations.types.createBaseVisualization(vis(visualizationDependencies)) + ); } public start(core: CoreStart, deps: VisTypeVislibPluginStartDependencies) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap index 9b2b633fd0e16..c2bac1c1e706a 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`VisLegend Component Legend closed should match the snapshot 1`] = `"<div class=\\"visLegend\\"><button type=\\"button\\" class=\\"visLegend__toggle kbn-resetFocusState\\" aria-label=\\"Toggle legend\\" aria-expanded=\\"false\\" aria-controls=\\"legendId\\" data-test-subj=\\"vislibToggleLegend\\" title=\\"Toggle legend\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon--text euiIcon-isLoaded\\" focusable=\\"false\\" role=\\"img\\" aria-hidden=\\"true\\"><title>"`; +exports[`VisLegend Component Legend closed should match the snapshot 1`] = `"
"`; -exports[`VisLegend Component Legend open should match the snapshot 1`] = `"
"`; +exports[`VisLegend Component Legend open should match the snapshot 1`] = `"
"`; diff --git a/src/legacy/core_plugins/vis_type_xy/index.ts b/src/legacy/core_plugins/vis_type_xy/index.ts new file mode 100644 index 0000000000000..975399f891503 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_xy/index.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 { resolve } from 'path'; +import { Legacy } from 'kibana'; + +import { LegacyPluginApi, LegacyPluginInitializer } from '../../types'; + +export interface ConfigSchema { + visTypeXy: { + enabled: boolean; + }; +} + +const visTypeXyPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => + new Plugin({ + id: 'visTypeXy', + require: ['kibana', 'elasticsearch', 'visualizations', 'interpreter', 'data'], + publicDir: resolve(__dirname, 'public'), + uiExports: { + hacks: [resolve(__dirname, 'public/legacy')], + injectDefaultVars(server): ConfigSchema { + const config = server.config(); + + return { + visTypeXy: { + enabled: config.get('visTypeXy.enabled') as boolean, + }, + }; + }, + }, + config(Joi: any) { + return Joi.object({ + enabled: Joi.boolean().default(false), + }).default(); + }, + } as Legacy.PluginSpecOptions); + +// eslint-disable-next-line import/no-default-export +export default visTypeXyPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_xy/package.json b/src/legacy/core_plugins/vis_type_xy/package.json new file mode 100644 index 0000000000000..920f7dcb44e87 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_xy/package.json @@ -0,0 +1,4 @@ +{ + "name": "visTypeXy", + "version": "kibana" +} diff --git a/src/legacy/core_plugins/vis_type_xy/public/index.ts b/src/legacy/core_plugins/vis_type_xy/public/index.ts new file mode 100644 index 0000000000000..218dc8aa8a683 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_xy/public/index.ts @@ -0,0 +1,25 @@ +/* + * 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 { PluginInitializerContext } from '../../../../core/public'; +import { VisTypeXyPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/legacy/core_plugins/vis_type_xy/public/legacy.ts b/src/legacy/core_plugins/vis_type_xy/public/legacy.ts new file mode 100644 index 0000000000000..e1cee9c30804a --- /dev/null +++ b/src/legacy/core_plugins/vis_type_xy/public/legacy.ts @@ -0,0 +1,44 @@ +/* + * 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 { npSetup, npStart } from 'ui/new_platform'; +import { PluginInitializerContext } from 'kibana/public'; + +import { plugin } from '.'; +import { VisTypeXyPluginSetupDependencies, VisTypeXyPluginStartDependencies } from './plugin'; +import { + setup as visualizationsSetup, + start as visualizationsStart, +} from '../../visualizations/public/np_ready/public/legacy'; + +const setupPlugins: Readonly = { + expressions: npSetup.plugins.expressions, + visualizations: visualizationsSetup, + charts: npSetup.plugins.charts, +}; + +const startPlugins: Readonly = { + expressions: npStart.plugins.expressions, + visualizations: visualizationsStart, +}; + +const pluginInstance = plugin({} as PluginInitializerContext); + +export const setup = pluginInstance.setup(npSetup.core, setupPlugins); +export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/legacy/core_plugins/vis_type_xy/public/plugin.ts b/src/legacy/core_plugins/vis_type_xy/public/plugin.ts new file mode 100644 index 0000000000000..59bb64b337256 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_xy/public/plugin.ts @@ -0,0 +1,82 @@ +/* + * 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 { + CoreSetup, + CoreStart, + Plugin, + IUiSettingsClient, + PluginInitializerContext, +} from 'kibana/public'; + +import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; +import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public'; +import { ChartsPluginSetup } from '../../../../plugins/charts/public'; + +export interface VisTypeXyDependencies { + uiSettings: IUiSettingsClient; + charts: ChartsPluginSetup; +} + +/** @internal */ +export interface VisTypeXyPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + charts: ChartsPluginSetup; +} + +/** @internal */ +export interface VisTypeXyPluginStartDependencies { + expressions: ReturnType; + visualizations: VisualizationsStart; +} + +type VisTypeXyCoreSetup = CoreSetup; + +/** @internal */ +export class VisTypeXyPlugin implements Plugin, void> { + constructor(public initializerContext: PluginInitializerContext) {} + + public async setup( + core: VisTypeXyCoreSetup, + { expressions, visualizations, charts }: VisTypeXyPluginSetupDependencies + ) { + // eslint-disable-next-line no-console + console.warn( + 'The visTypeXy plugin is enabled\n\n', + 'This may negatively alter existing vislib visualization configurations if saved.' + ); + const visualizationDependencies: Readonly = { + uiSettings: core.uiSettings, + charts, + }; + + const visTypeDefinitions: any[] = []; + const visFunctions: any = []; + + visFunctions.forEach((fn: any) => expressions.registerFunction(fn)); + visTypeDefinitions.forEach((vis: any) => + visualizations.types.createBaseVisualization(vis(visualizationDependencies)) + ); + } + + public start(core: CoreStart, deps: VisTypeXyPluginStartDependencies) { + // nothing to do here + } +} diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 88a794445870c..f2a14df1d1eb3 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -257,9 +257,9 @@ export default () => manifestServiceUrl: Joi.string() .default('') .allow(''), - emsFileApiUrl: Joi.string().default('https://vector-staging.maps.elastic.co'), + emsFileApiUrl: Joi.string().default('https://vector.maps.elastic.co'), emsTileApiUrl: Joi.string().default('https://tiles.maps.elastic.co'), - emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v7.4'), + emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v7.6'), emsFontLibraryUrl: Joi.string().default( 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf' ), diff --git a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts index c89ae9f8b3c9b..439ac9b5713df 100644 --- a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts +++ b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts @@ -27,6 +27,7 @@ import { inspectorPluginMock } from '../../../../../plugins/inspector/public/moc import { uiActionsPluginMock } from '../../../../../plugins/ui_actions/public/mocks'; import { managementPluginMock } from '../../../../../plugins/management/public/mocks'; import { usageCollectionPluginMock } from '../../../../../plugins/usage_collection/public/mocks'; +import { kibanaLegacyPluginMock } from '../../../../../plugins/kibana_legacy/public/mocks'; import { chartPluginMock } from '../../../../../plugins/charts/public/mocks'; /* eslint-enable @kbn/eslint/no-restricted-paths */ @@ -40,6 +41,7 @@ export const pluginsMock = { expressions: expressionsPluginMock.createSetupContract(), uiActions: uiActionsPluginMock.createSetupContract(), usageCollection: usageCollectionPluginMock.createSetupContract(), + kibana_legacy: kibanaLegacyPluginMock.createSetupContract(), }), createStart: () => ({ data: dataPluginMock.createStartContract(), @@ -50,6 +52,7 @@ export const pluginsMock = { expressions: expressionsPluginMock.createStartContract(), uiActions: uiActionsPluginMock.createStartContract(), management: managementPluginMock.createStartContract(), + kibana_legacy: kibanaLegacyPluginMock.createStartContract(), }), }; diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index f98b8801d5266..c2c8b5a0fae7a 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -119,6 +119,9 @@ export const npSetup = { kibana_legacy: { registerLegacyApp: () => {}, forwardApp: () => {}, + config: { + defaultAppId: 'home', + }, }, inspector: { registerView: () => undefined, @@ -140,6 +143,9 @@ export const npSetup = { environment: { update: sinon.fake(), }, + config: { + disableWelcomeScreen: false, + }, }, charts: { theme: { @@ -196,6 +202,9 @@ export const npStart = { kibana_legacy: { getApps: () => [], getForwards: () => [], + config: { + defaultAppId: 'home', + }, }, data: { autocomplete: { @@ -297,6 +306,9 @@ export const npStart = { environment: { get: sinon.fake(), }, + config: { + disableWelcomeScreen: false, + }, }, navigation: { ui: { diff --git a/src/plugins/home/config.ts b/src/plugins/home/config.ts new file mode 100644 index 0000000000000..149723a7ee5ae --- /dev/null +++ b/src/plugins/home/config.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. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + disableWelcomeScreen: schema.boolean({ defaultValue: false }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index ca05c8b5f760e..114d442b40943 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import { PluginInitializerContext } from 'kibana/public'; + export { FeatureCatalogueSetup, FeatureCatalogueStart, @@ -26,4 +28,5 @@ export { export { FeatureCatalogueEntry, FeatureCatalogueCategory, Environment } from './services'; import { HomePublicPlugin } from './plugin'; -export const plugin = () => new HomePublicPlugin(); +export const plugin = (initializerContext: PluginInitializerContext) => + new HomePublicPlugin(initializerContext); diff --git a/src/plugins/home/public/plugin.test.ts b/src/plugins/home/public/plugin.test.ts index 34502d7d2c6cd..fa44a110c63b7 100644 --- a/src/plugins/home/public/plugin.test.ts +++ b/src/plugins/home/public/plugin.test.ts @@ -19,6 +19,9 @@ import { registryMock, environmentMock } from './plugin.test.mocks'; import { HomePublicPlugin } from './plugin'; +import { coreMock } from '../../../core/public/mocks'; + +const mockInitializerContext = coreMock.createPluginInitializerContext(); describe('HomePublicPlugin', () => { beforeEach(() => { @@ -30,13 +33,13 @@ describe('HomePublicPlugin', () => { describe('setup', () => { test('wires up and returns registry', async () => { - const setup = await new HomePublicPlugin().setup(); + const setup = await new HomePublicPlugin(mockInitializerContext).setup(); expect(setup).toHaveProperty('featureCatalogue'); expect(setup.featureCatalogue).toHaveProperty('register'); }); test('wires up and returns environment service', async () => { - const setup = await new HomePublicPlugin().setup(); + const setup = await new HomePublicPlugin(mockInitializerContext).setup(); expect(setup).toHaveProperty('environment'); expect(setup.environment).toHaveProperty('update'); }); @@ -44,7 +47,7 @@ describe('HomePublicPlugin', () => { describe('start', () => { test('wires up and returns registry', async () => { - const service = new HomePublicPlugin(); + const service = new HomePublicPlugin(mockInitializerContext); await service.setup(); const core = { application: { capabilities: { catalogue: {} } } } as any; const start = await service.start(core); @@ -55,7 +58,7 @@ describe('HomePublicPlugin', () => { }); test('wires up and returns environment service', async () => { - const service = new HomePublicPlugin(); + const service = new HomePublicPlugin(mockInitializerContext); await service.setup(); const start = await service.start({ application: { capabilities: { catalogue: {} } }, diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 39a7f23826900..fe68dbc3e7e49 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -17,7 +17,8 @@ * under the License. */ -import { CoreStart, Plugin } from 'src/core/public'; +import { CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; + import { EnvironmentService, EnvironmentServiceSetup, @@ -26,19 +27,23 @@ import { FeatureCatalogueRegistrySetup, FeatureCatalogueRegistryStart, } from './services'; +import { ConfigSchema } from '../config'; export class HomePublicPlugin implements Plugin { private readonly featuresCatalogueRegistry = new FeatureCatalogueRegistry(); private readonly environmentService = new EnvironmentService(); - public async setup() { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public setup(): HomePublicPluginSetup { return { featureCatalogue: { ...this.featuresCatalogueRegistry.setup() }, environment: { ...this.environmentService.setup() }, + config: this.initializerContext.config.get(), }; } - public async start(core: CoreStart) { + public start(core: CoreStart): HomePublicPluginStart { return { featureCatalogue: { ...this.featuresCatalogueRegistry.start({ @@ -71,6 +76,7 @@ export interface HomePublicPluginSetup { * @deprecated */ environment: EnvironmentSetup; + config: ConfigSchema; } /** @public */ diff --git a/src/plugins/home/server/index.ts b/src/plugins/home/server/index.ts index 0961c729698b9..02f4c91a414cc 100644 --- a/src/plugins/home/server/index.ts +++ b/src/plugins/home/server/index.ts @@ -20,8 +20,19 @@ export { HomeServerPluginSetup, HomeServerPluginStart } from './plugin'; export { TutorialProvider } from './services'; export { SampleDatasetProvider, SampleDataRegistrySetup } from './services'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server'; import { HomeServerPlugin } from './plugin'; +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + disableWelcomeScreen: true, + }, + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('kibana.disableWelcomeScreen', 'home.disableWelcomeScreen'), + ], +}; export const plugin = (initContext: PluginInitializerContext) => new HomeServerPlugin(initContext); diff --git a/src/plugins/home/server/plugin.ts b/src/plugins/home/server/plugin.ts index 23c236764cddc..d2f2d7041024e 100644 --- a/src/plugins/home/server/plugin.ts +++ b/src/plugins/home/server/plugin.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { TutorialsRegistry, TutorialsRegistrySetup, diff --git a/src/plugins/kibana_legacy/config.ts b/src/plugins/kibana_legacy/config.ts new file mode 100644 index 0000000000000..291f8813ecfb9 --- /dev/null +++ b/src/plugins/kibana_legacy/config.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. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + defaultAppId: schema.string({ defaultValue: 'home' }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/kibana_legacy/kibana.json b/src/plugins/kibana_legacy/kibana.json index 26ee6db3ba06a..b6d11309a4f96 100644 --- a/src/plugins/kibana_legacy/kibana.json +++ b/src/plugins/kibana_legacy/kibana.json @@ -1,6 +1,6 @@ { "id": "kibana_legacy", "version": "kibana", - "server": false, + "server": true, "ui": true } diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts index 4cb30be8917ac..de8788808e74c 100644 --- a/src/plugins/kibana_legacy/public/index.ts +++ b/src/plugins/kibana_legacy/public/index.ts @@ -20,8 +20,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { KibanaLegacyPlugin } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { - return new KibanaLegacyPlugin(); -} +export const plugin = (initializerContext: PluginInitializerContext) => + new KibanaLegacyPlugin(initializerContext); export * from './plugin'; diff --git a/src/plugins/kibana_legacy/public/mocks.ts b/src/plugins/kibana_legacy/public/mocks.ts new file mode 100644 index 0000000000000..b6287dd9d9a55 --- /dev/null +++ b/src/plugins/kibana_legacy/public/mocks.ts @@ -0,0 +1,44 @@ +/* + * 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 { KibanaLegacyPlugin } from './plugin'; + +export type Setup = jest.Mocked>; +export type Start = jest.Mocked>; + +const createSetupContract = (): Setup => ({ + forwardApp: jest.fn(), + registerLegacyApp: jest.fn(), + config: { + defaultAppId: 'home', + }, +}); + +const createStartContract = (): Start => ({ + getApps: jest.fn(), + getForwards: jest.fn(), + config: { + defaultAppId: 'home', + }, +}); + +export const kibanaLegacyPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index cb95088320d7b..b9a61a1c9b200 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -17,7 +17,9 @@ * under the License. */ -import { App } from 'kibana/public'; +import { App, PluginInitializerContext } from 'kibana/public'; + +import { ConfigSchema } from '../config'; interface ForwardDefinition { legacyAppId: string; @@ -29,6 +31,8 @@ export class KibanaLegacyPlugin { private apps: App[] = []; private forwards: ForwardDefinition[] = []; + constructor(private readonly initializerContext: PluginInitializerContext) {} + public setup() { return { /** @@ -77,6 +81,8 @@ export class KibanaLegacyPlugin { ) => { this.forwards.push({ legacyAppId, newAppId, ...options }); }, + + config: this.initializerContext.config.get(), }; } @@ -92,6 +98,7 @@ export class KibanaLegacyPlugin { * Just exported for wiring up with legacy platform, should not be used. */ getForwards: () => this.forwards, + config: this.initializerContext.config.get(), }; } } diff --git a/src/plugins/kibana_legacy/server/index.ts b/src/plugins/kibana_legacy/server/index.ts new file mode 100644 index 0000000000000..4d0fe8364a66c --- /dev/null +++ b/src/plugins/kibana_legacy/server/index.ts @@ -0,0 +1,41 @@ +/* + * 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 { CoreSetup, CoreStart, PluginConfigDescriptor } from 'kibana/server'; + +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + defaultAppId: true, + }, + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + // TODO: Remove deprecation once defaultAppId is deleted + renameFromRoot('kibana.defaultAppId', 'kibana_legacy.defaultAppId', true), + ], +}; + +class Plugin { + public setup(core: CoreSetup) {} + + public start(core: CoreStart) {} +} + +export const plugin = () => new Plugin(); diff --git a/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap b/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap index 18f84f41d5d99..307c0760de7ba 100644 --- a/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap +++ b/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap @@ -8,7 +8,6 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` diff --git a/src/plugins/kibana_utils/public/core/create_getter_setter.ts b/src/plugins/kibana_utils/common/create_getter_setter.ts similarity index 100% rename from src/plugins/kibana_utils/public/core/create_getter_setter.ts rename to src/plugins/kibana_utils/common/create_getter_setter.ts diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index d4aeb2c0fe4ad..444c10194a8e3 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -20,4 +20,5 @@ export * from './defer'; export * from './of'; export * from './state_containers'; +export { createGetterSetter, Get, Set } from './create_getter_setter'; export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; diff --git a/src/plugins/kibana_utils/public/core/create_kibana_utils_core.ts b/src/plugins/kibana_utils/public/core/create_kibana_utils_core.ts index 84ecffa1da634..c528c68f29edb 100644 --- a/src/plugins/kibana_utils/public/core/create_kibana_utils_core.ts +++ b/src/plugins/kibana_utils/public/core/create_kibana_utils_core.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter, Get, Set } from './create_getter_setter'; +import { createGetterSetter, Get, Set } from '../../common'; import { CoreStart } from '../../../../core/public'; import { KUSavedObjectClient, createSavedObjectsClient } from './saved_objects_client'; diff --git a/src/plugins/kibana_utils/public/core/index.ts b/src/plugins/kibana_utils/public/core/index.ts index 7e8dff7191fe8..3f08d591300a2 100644 --- a/src/plugins/kibana_utils/public/core/index.ts +++ b/src/plugins/kibana_utils/public/core/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export * from './create_getter_setter'; export * from './create_kibana_utils_core'; diff --git a/src/plugins/kibana_utils/public/core/saved_objects_client.ts b/src/plugins/kibana_utils/public/core/saved_objects_client.ts index 40407fea5d189..5262755a6a3ab 100644 --- a/src/plugins/kibana_utils/public/core/saved_objects_client.ts +++ b/src/plugins/kibana_utils/public/core/saved_objects_client.ts @@ -18,7 +18,7 @@ */ import { CoreStart } from '../../../../core/public'; -import { Get } from './create_getter_setter'; +import { Get } from '../../common'; type CoreSavedObjectClient = CoreStart['savedObjects']['client']; diff --git a/src/plugins/kibana_utils/public/core/state.ts b/src/plugins/kibana_utils/public/core/state.ts index 8ac6e4e0e58e6..52c4d166d737e 100644 --- a/src/plugins/kibana_utils/public/core/state.ts +++ b/src/plugins/kibana_utils/public/core/state.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from './create_getter_setter'; +import { createGetterSetter } from '../../common'; import { CoreStart } from '../../../../core/public'; export const [getCoreStart, setCoreStart] = createGetterSetter('CoreStart'); diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 78828ad9c4b2d..5b6d304e14c2e 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -17,7 +17,7 @@ * under the License. */ -export { defer, Defer, of } from '../common'; +export { defer, Defer, of, createGetterSetter, Get, Set } from '../common'; export * from './core'; export * from './errors'; export * from './field_mapping'; diff --git a/src/plugins/kibana_utils/server/index.ts b/src/plugins/kibana_utils/server/index.ts new file mode 100644 index 0000000000000..f8b79a1b8b339 --- /dev/null +++ b/src/plugins/kibana_utils/server/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 { Get, Set, createGetterSetter } from '../common'; diff --git a/src/plugins/management/public/management_service.test.ts b/src/plugins/management/public/management_service.test.ts index 854406a10335b..b34e76474cec2 100644 --- a/src/plugins/management/public/management_service.test.ts +++ b/src/plugins/management/public/management_service.test.ts @@ -19,12 +19,13 @@ import { ManagementService } from './management_service'; import { coreMock } from '../../../core/public/mocks'; +import { npSetup } from '../../../legacy/ui/public/new_platform/__mocks__'; -const mockKibanaLegacy = { registerLegacyApp: () => {}, forwardApp: () => {} }; +jest.mock('ui/new_platform'); test('Provides default sections', () => { const service = new ManagementService().setup( - mockKibanaLegacy, + npSetup.plugins.kibana_legacy, () => {}, coreMock.createSetup().getStartServices ); @@ -36,7 +37,7 @@ test('Provides default sections', () => { test('Register section, enable and disable', () => { const service = new ManagementService().setup( - mockKibanaLegacy, + npSetup.plugins.kibana_legacy, () => {}, coreMock.createSetup().getStartServices ); diff --git a/test/common/config.js b/test/common/config.js index 29d4bbf10a6ce..faf8cef027170 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -54,7 +54,7 @@ export default function() { `--elasticsearch.hosts=${formatUrl(servers.elasticsearch)}`, `--elasticsearch.username=${kibanaServerTestUser.username}`, `--elasticsearch.password=${kibanaServerTestUser.password}`, - `--kibana.disableWelcomeScreen=true`, + `--home.disableWelcomeScreen=true`, '--telemetry.banner=false', `--server.maxPayloadBytes=1679958`, // newsfeed mock service diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index d96febee7b06d..d320b57ee59e6 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0", "react-dom": "^16.12.0" } diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts index 657d8d5150c3a..a700727d87299 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from '../../../../../../src/plugins/kibana_utils/public/core'; +import { createGetterSetter } from '../../../../../../src/plugins/kibana_utils/public'; import { ExpressionsStart } from './types'; export const [getExpressions, setExpressions] = createGetterSetter('Expressions'); diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index 170cc77ca37cc..27a8c1fab6c8e 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0" } } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index 85c76071d1e94..51bb7240dd7c4 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0" }, "scripts": { diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index ade93c9f50099..9ee0e3de51d8b 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0" }, "scripts": { diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index 9b8cfe9b69ed0..a5d9f6fb6e6a6 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -1436,97 +1436,120 @@ describe('find()', () => { }); describe('delete()', () => { - test('successfully removes an alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - scheduledTaskId: 'task-123', - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], + let alertsClient: AlertsClient; + const existingAlert = { + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, }, - references: [ + scheduledTaskId: 'task-123', + actions: [ { - name: 'action_0', - type: 'action', - id: '1', + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, }, ], - }); - savedObjectsClient.delete.mockResolvedValueOnce({ + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + savedObjectsClient.get.mockResolvedValue(existingAlert); + savedObjectsClient.delete.mockResolvedValue({ success: true, }); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + }); + }); + + test('successfully removes an alert', async () => { const result = await alertsClient.delete({ id: '1' }); expect(result).toEqual({ success: true }); - expect(savedObjectsClient.delete).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - ] - `); - expect(taskManager.remove).toHaveBeenCalledTimes(1); - expect(taskManager.remove.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "task-123", - ] - `); + expect(savedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); + expect(taskManager.remove).toHaveBeenCalledWith('task-123'); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); }); - test('swallows error when invalidate API key throws', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', + test(`doesn't remove a task when scheduledTaskId is null`, async () => { + savedObjectsClient.get.mockResolvedValue({ + ...existingAlert, attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - apiKey: Buffer.from('123:abc').toString('base64'), - scheduledTaskId: 'task-123', - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], + ...existingAlert.attributes, + scheduledTaskId: null, }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], }); - savedObjectsClient.delete.mockResolvedValueOnce({ - success: true, + + await alertsClient.delete({ id: '1' }); + expect(taskManager.remove).not.toHaveBeenCalled(); + }); + + test(`doesn't invalidate API key when apiKey is null`, async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: null, + }, }); await alertsClient.delete({ id: '1' }); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + }); + + test('swallows error when invalidate API key throws', async () => { + alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); + + await alertsClient.delete({ id: '1' }); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'Failed to invalidate API Key: Fail' ); }); + + test('swallows error when getDecryptedAsInternalUser throws an error', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + + await alertsClient.delete({ id: '1' }); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'delete(): Failed to load API key to invalidate on alert 1: Fail' + ); + }); + + test('throws error when savedObjectsClient.get throws an error', async () => { + savedObjectsClient.get.mockRejectedValue(new Error('SOC Fail')); + + await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"SOC Fail"` + ); + }); + + test('throws error when taskManager.remove throws an error', async () => { + taskManager.remove.mockRejectedValue(new Error('TM Fail')); + + await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"TM Fail"` + ); + }); }); describe('update()', () => { diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index 1346d403edda1..12d60b162d58e 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -226,14 +226,29 @@ export class AlertsClient { } public async delete({ id }: { id: string }) { - const decryptedAlertSavedObject = await this.encryptedSavedObjectsPlugin.getDecryptedAsInternalUser< - RawAlert - >('alert', id, { namespace: this.namespace }); + const [taskIdToRemove, apiKeyToInvalidate] = await Promise.all([ + this.savedObjectsClient + .get('alert', id) + .then(result => result.attributes.scheduledTaskId), + // We'll try and load the decrypted saved object but if this fails we'll only log + // and skip invalidating the API key. + this.encryptedSavedObjectsPlugin + .getDecryptedAsInternalUser('alert', id, { namespace: this.namespace }) + .then(result => result.attributes.apiKey) + .catch(e => + this.logger.error( + `delete(): Failed to load API key to invalidate on alert ${id}: ${e.message}` + ) + ), + ]); + const removeResult = await this.savedObjectsClient.delete('alert', id); - if (decryptedAlertSavedObject.attributes.scheduledTaskId) { - await this.taskManager.remove(decryptedAlertSavedObject.attributes.scheduledTaskId); - } - await this.invalidateApiKey({ apiKey: decryptedAlertSavedObject.attributes.apiKey }); + + await Promise.all([ + taskIdToRemove && this.taskManager.remove(taskIdToRemove), + apiKeyToInvalidate && this.invalidateApiKey({ apiKey: apiKeyToInvalidate }), + ]); + return removeResult; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap index b2c503806c385..260d7de3aefd4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap @@ -62,11 +62,7 @@ exports[`DetailView should render TabContent 1`] = ` `; exports[`DetailView should render tabs 1`] = ` - + List should render empty state 1`] = ` } initialPageSize={25} initialSortDirection="desc" - initialSortField="latestOccurrenceAt" + initialSortField="occurrenceCount" items={Array []} noItemsMessage="No errors were found" sortItems={false} @@ -190,7 +190,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` List should render empty state 1`] = ` } >
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with height specified 1`] = `"
"`; @@ -21,7 +21,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with height specified
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with page specified 1`] = `"
"`; @@ -33,7 +33,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with page specified 2`
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width and height specified 1`] = `"
"`; @@ -45,7 +45,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with width and height
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width specified 1`] = `"
"`; @@ -57,5 +57,5 @@ exports[`Canvas Shareable Workpad API Placed successfully with width specified 2
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap index 8a8799207ace8..73d7599a60359 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap @@ -65,7 +65,6 @@ exports[` can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -85,7 +84,6 @@ exports[`<Settings /> can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -110,7 +108,6 @@ exports[`<Settings /> can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -130,7 +127,6 @@ exports[`<Settings /> can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -219,7 +215,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -239,7 +234,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -264,7 +258,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -284,7 +277,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -317,7 +309,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M10.843 13.069L6.232 8.384a.546.546 0 010-.768l4.61-4.685a.552.552 0 000-.771.53.53 0 00-.759 0l-4.61 4.684a1.65 1.65 0 000 2.312l4.61 4.684a.53.53 0 00.76 0 .552.552 0 000-.771z" fill-rule="nonzero" @@ -366,7 +357,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M7.293 8L3.146 3.854a.5.5 0 11.708-.708L8 7.293l4.146-4.147a.5.5 0 01.708.708L8.707 8l4.147 4.146a.5.5 0 01-.708.708L8 8.707l-4.146 4.147a.5.5 0 01-.708-.708L7.293 8z" /> @@ -381,7 +371,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M6.5 12a.502.502 0 01-.354-.146l-4-4a.502.502 0 01.708-.708L6.5 10.793l6.646-6.647a.502.502 0 01.708.708l-7 7A.502.502 0 016.5 12" fill-rule="evenodd" @@ -561,7 +550,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -581,7 +569,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -606,7 +593,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -626,7 +612,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -715,7 +700,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -735,7 +719,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -760,7 +743,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -780,7 +762,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -813,7 +794,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M10.843 13.069L6.232 8.384a.546.546 0 010-.768l4.61-4.685a.552.552 0 000-.771.53.53 0 00-.759 0l-4.61 4.684a1.65 1.65 0 000 2.312l4.61 4.684a.53.53 0 00.76 0 .552.552 0 000-.771z" fill-rule="nonzero" @@ -871,7 +851,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M7.293 8L3.146 3.854a.5.5 0 11.708-.708L8 7.293l4.146-4.147a.5.5 0 01.708.708L8.707 8l4.147 4.146a.5.5 0 01-.708.708L8 8.707l-4.146 4.147a.5.5 0 01-.708-.708L7.293 8z" /> @@ -886,7 +865,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M6.5 12a.502.502 0 01-.354-.146l-4-4a.502.502 0 01.708-.708L6.5 10.793l6.646-6.647a.502.502 0 01.708.708l-7 7A.502.502 0 016.5 12" fill-rule="evenodd" @@ -927,4 +905,4 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = </div> `; -exports[`<Settings /> can navigate Toolbar Settings, closes when activated 3`] = `"<div><div><div data-focus-guard=\\"true\\" tabindex=\\"-1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-guard=\\"true\\" tabindex=\\"-1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-lock-disabled=\\"disabled\\"><div class=\\"euiPanel euiPopover__panel euiPopover__panel--top euiPopover__panel-withTitle\\" aria-live=\\"assertive\\" role=\\"dialog\\" aria-modal=\\"true\\" aria-describedby=\\"generated-id\\" style=\\"top: -16px; left: -22px; z-index: 2000;\\"><div class=\\"euiPopover__panelArrow euiPopover__panelArrow--top\\" style=\\"left: 10px; top: 0px;\\"></div><div><div class=\\"euiContextMenu\\" style=\\"height: 0px;\\"><div class=\\"euiContextMenuPanel euiContextMenu__panel euiContextMenuPanel-txOutLeft\\" tabindex=\\"0\\"><div class=\\"euiPopoverTitle\\"><span class=\\"euiContextMenu__itemLayout\\">Settings</span></div><div><div><button class=\\"euiContextMenuItem\\" type=\\"button\\"><span class=\\"euiContextMenu__itemLayout\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoaded euiContextMenu__icon\\" focusable=\\"false\\" role=\\"img\\" aria-hidden=\\"true\\"><title>Auto Play
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; +exports[` can navigate Toolbar Settings, closes when activated 3`] = `"
Settings
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; diff --git a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js index ebae49f994723..4215f96c8de4a 100644 --- a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js @@ -55,10 +55,12 @@ chrome.setRootController('kibana', function() { uiModules.get('kibana').run(showAppRedirectNotification); -// If there is a configured kbnDefaultAppId, and it is a dashboard ID, we'll -// show that dashboard, otherwise, we'll show the default dasbhoard landing page. +/** + * If there is a configured `kibana.defaultAppId`, and it is a dashboard ID, we'll + * show that dashboard, otherwise, we'll show the default dasbhoard landing page. + */ function defaultUrl() { - const defaultAppId = chrome.getInjected('kbnDefaultAppId', ''); + const defaultAppId = npStart.plugins.kibana_legacy.config.defaultAppId || ''; const isDashboardId = defaultAppId.startsWith(dashboardAppIdPrefix()); return isDashboardId ? `/${defaultAppId}` : DashboardConstants.LANDING_PAGE_PATH; } diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.js b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.js index e71820a346f91..7534f3cd0934e 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.js @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; export function checkLicense(xpackLicenseInfo) { - const pluginName = 'Index Management'; + const pluginName = 'Index Lifecycle Policies'; // If, for some reason, we cannot get the license information // from Elasticsearch, assume worst case and disable diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx index 80a7ceb61c324..99926c646da22 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx @@ -60,10 +60,14 @@ function createMockFilterManager() { return unsubscribe; }, }), - setFilters: (newFilters: unknown[]) => { + setFilters: jest.fn((newFilters: unknown[]) => { filters = newFilters; - subscriber(); - }, + if (subscriber) subscriber(); + }), + setAppFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), getFilters: () => filters, getGlobalFilters: () => { // @ts-ignore @@ -189,6 +193,13 @@ describe('Lens App', () => { `); }); + it('clears app filters on load', () => { + const defaultArgs = makeDefaultArgs(); + mount(); + + expect(defaultArgs.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([]); + }); + it('sets breadcrumbs when the document title changes', async () => { const defaultArgs = makeDefaultArgs(); const instance = mount(); @@ -226,7 +237,7 @@ describe('Lens App', () => { expect(args.docStorage.load).not.toHaveBeenCalled(); }); - it('loads a document and uses query if there is a document id', async () => { + it('loads a document and uses query and filters if there is a document id', async () => { const args = makeDefaultArgs(); args.editorFrame = frame; (args.docStorage.load as jest.Mock).mockResolvedValue({ @@ -234,6 +245,7 @@ describe('Lens App', () => { expression: 'valid expression', state: { query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, }); @@ -245,6 +257,9 @@ describe('Lens App', () => { expect(args.docStorage.load).toHaveBeenCalledWith('1234'); expect(args.data.indexPatterns.get).toHaveBeenCalledWith('1'); + expect(args.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([ + { query: { match_phrase: { src: 'test' } } }, + ]); expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: 'fake query', @@ -260,6 +275,7 @@ describe('Lens App', () => { expression: 'valid expression', state: { query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, }, diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index 35e45af6a3d68..6d2ebee1d88db 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -83,6 +83,10 @@ export function App({ const { lastKnownDoc } = state; useEffect(() => { + // Clear app-specific filters when navigating to Lens. Necessary because Lens + // can be loaded without a full page refresh + data.query.filterManager.setAppFilters([]); + const filterSubscription = data.query.filterManager.getUpdates$().subscribe({ next: () => { setState(s => ({ ...s, filters: data.query.filterManager.getFilters() })); @@ -123,13 +127,14 @@ export function App({ core.notifications ) .then(indexPatterns => { + // Don't overwrite any pinned filters + data.query.filterManager.setAppFilters(doc.state.filters); setState(s => ({ ...s, isLoading: false, persistedDoc: doc, lastKnownDoc: doc, query: doc.state.query, - filters: doc.state.filters, indexPatternsForTopNav: indexPatterns, })); }) diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 9256bee4e756b..353dc58e6d401 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -296,7 +296,6 @@ exports[`UploadLicense should display a modal when license requires acknowledgem } > @@ -1401,7 +1399,6 @@ exports[`UploadLicense should display an error when ES says license is expired 1 width={16} xmlns="http://www.w3.org/2000/svg" > - <path d="M9 10.114l1.85-1.943a.52.52 0 01.77 0c.214.228.214.6 0 .829l-1.95 2.05a1.552 1.552 0 01-2.31 0L5.41 9a.617.617 0 010-.829.52.52 0 01.77 0L8 10.082V1.556C8 1.249 8.224 1 8.5 1s.5.249.5.556v8.558zM4.18 6a.993.993 0 00-.972.804l-1.189 6A.995.995 0 002.991 14h11.018a1 1 0 00.972-1.196l-1.19-6a.993.993 0 00-.97-.804H4.18zM6 5v1h5V5h1.825c.946 0 1.76.673 1.946 1.608l1.19 6A2 2 0 0114.016 15H2.984a1.992 1.992 0 01-1.945-2.392l1.19-6C2.414 5.673 3.229 5 4.174 5H6z" /> @@ -1871,7 +1868,6 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 width={16} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M9 10.114l1.85-1.943a.52.52 0 01.77 0c.214.228.214.6 0 .829l-1.95 2.05a1.552 1.552 0 01-2.31 0L5.41 9a.617.617 0 010-.829.52.52 0 01.77 0L8 10.082V1.556C8 1.249 8.224 1 8.5 1s.5.249.5.556v8.558zM4.18 6a.993.993 0 00-.972.804l-1.189 6A.995.995 0 002.991 14h11.018a1 1 0 00.972-1.196l-1.19-6a.993.993 0 00-.97-.804H4.18zM6 5v1h5V5h1.825c.946 0 1.76.673 1.946 1.608l1.19 6A2 2 0 0114.016 15H2.984a1.992 1.992 0 01-1.945-2.392l1.19-6C2.414 5.673 3.229 5 4.174 5H6z" /> @@ -2806,7 +2802,6 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` width={16} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M9 10.114l1.85-1.943a.52.52 0 01.77 0c.214.228.214.6 0 .829l-1.95 2.05a1.552 1.552 0 01-2.31 0L5.41 9a.617.617 0 010-.829.52.52 0 01.77 0L8 10.082V1.556C8 1.249 8.224 1 8.5 1s.5.249.5.556v8.558zM4.18 6a.993.993 0 00-.972.804l-1.189 6A.995.995 0 002.991 14h11.018a1 1 0 00.972-1.196l-1.19-6a.993.993 0 00-.97-.804H4.18zM6 5v1h5V5h1.825c.946 0 1.76.673 1.946 1.608l1.19 6A2 2 0 0114.016 15H2.984a1.992 1.992 0 01-1.945-2.392l1.19-6C2.414 5.673 3.229 5 4.174 5H6z" /> diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap index 2dc355513ece2..c62b07a89e7a3 100644 --- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap @@ -33,6 +33,7 @@ exports[`should not render relation select when geo field is geo_point 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ @@ -110,6 +111,7 @@ exports[`should not show "within" relation when filter geometry is not closed 1` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ @@ -214,6 +216,7 @@ exports[`should render error message 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ @@ -294,6 +297,7 @@ exports[`should render relation select when geo field is geo_shape 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap index 9a55e46b40aea..9d07b9c641e0f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap @@ -15,6 +15,7 @@ exports[`HeatmapStyleEditor is rendered 1`] = ` fullWidth={false} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ diff --git a/x-pack/legacy/plugins/maps/server/routes.js b/x-pack/legacy/plugins/maps/server/routes.js index 2e5ea299b6f67..757750dbb0813 100644 --- a/x-pack/legacy/plugins/maps/server/routes.js +++ b/x-pack/legacy/plugins/maps/server/routes.js @@ -69,7 +69,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_FILES_API_PATH}/${EMS_FILES_DEFAULT_JSON_PATH}`, handler: async request => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); if (!request.query.id) { server.log('warning', 'Must supply id parameters to retrieve EMS file'); @@ -96,7 +97,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_TILE_PATH}`, handler: async (request, h) => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); if ( !request.query.id || @@ -127,8 +129,9 @@ export function initRoutes(server, licenseUid) { server.route({ method: 'GET', path: `${ROOT}/${EMS_CATALOGUE_PATH}`, - handler: async () => { - checkEMSProxyConfig(); + handler: async request => { + const { server } = request; + checkEMSProxyConfig(server); const main = await emsClient.getMainManifest(); const proxiedManifest = { @@ -157,8 +160,9 @@ export function initRoutes(server, licenseUid) { server.route({ method: 'GET', path: `${ROOT}/${EMS_FILES_CATALOGUE_PATH}/{emsVersion}/manifest`, - handler: async () => { - checkEMSProxyConfig(); + handler: async request => { + const { server } = request; + checkEMSProxyConfig(server); const file = await emsClient.getDefaultFileManifest(); const layers = file.layers.map(layer => { @@ -181,8 +185,9 @@ export function initRoutes(server, licenseUid) { server.route({ method: 'GET', path: `${ROOT}/${EMS_TILES_CATALOGUE_PATH}/{emsVersion}/manifest`, - handler: async () => { - checkEMSProxyConfig(); + handler: async request => { + const { server } = request; + checkEMSProxyConfig(server); const tilesManifest = await emsClient.getDefaultTMSManifest(); const newServices = tilesManifest.services.map(service => { @@ -220,7 +225,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_STYLE_PATH}`, handler: async request => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); if (!request.query.id) { server.log('warning', 'Must supply id parameter to retrieve EMS raster style'); @@ -246,7 +252,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_STYLE_PATH}`, handler: async request => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); if (!request.query.id) { server.log('warning', 'Must supply id parameter to retrieve EMS vector style'); @@ -285,7 +292,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_SOURCE_PATH}`, handler: async request => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); if (!request.query.id || !request.query.sourceId) { server.log( @@ -316,7 +324,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_TILE_PATH}`, handler: async (request, h) => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); if ( !request.query.id || @@ -352,7 +361,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_GLYPHS_PATH}/{fontstack}/{range}`, handler: async (request, h) => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); const url = mapConfig.emsFontLibraryUrl .replace('{fontstack}', request.params.fontstack) .replace('{range}', request.params.range); @@ -365,7 +375,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_SPRITES_PATH}/{id}/sprite{scaling?}.{extension}`, handler: async (request, h) => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); if (!request.params.id) { server.log('warning', 'Must supply id parameter to retrieve EMS vector source sprite'); @@ -443,7 +454,7 @@ export function initRoutes(server, licenseUid) { }, }); - function checkEMSProxyConfig() { + function checkEMSProxyConfig(server) { if (!mapConfig.proxyElasticMapsServiceInMaps) { server.log( 'warning', diff --git a/x-pack/legacy/plugins/monitoring/common/constants.js b/x-pack/legacy/plugins/monitoring/common/constants.ts similarity index 85% rename from x-pack/legacy/plugins/monitoring/common/constants.js rename to x-pack/legacy/plugins/monitoring/common/constants.ts index ff16b0e9c5167..53764f592dc15 100644 --- a/x-pack/legacy/plugins/monitoring/common/constants.js +++ b/x-pack/legacy/plugins/monitoring/common/constants.ts @@ -233,3 +233,45 @@ export const REPORTING_SYSTEM_ID = 'reporting'; * @type {Number} */ export const TELEMETRY_COLLECTION_INTERVAL = 86400000; + +/** + * We want to slowly rollout the migration from watcher-based cluster alerts to + * kibana alerts and we only want to enable the kibana alerts once all + * watcher-based cluster alerts have been migrated so this flag will serve + * as the only way to see the new UI and actually run Kibana alerts. It will + * be false until all alerts have been migrated, then it will be removed + */ +export const KIBANA_ALERTING_ENABLED = false; + +/** + * The prefix for all alert types used by monitoring + */ +export const ALERT_TYPE_PREFIX = 'monitoring_'; + +/** + * This is the alert type id for the license expiration alert + */ +export const ALERT_TYPE_LICENSE_EXPIRATION = `${ALERT_TYPE_PREFIX}alert_type_license_expiration`; + +/** + * A listing of all alert types + */ +export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION]; + +/** + * Matches the id for the built-in in email action type + * See x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts + */ +export const ALERT_ACTION_TYPE_EMAIL = '.email'; + +/** + * The number of alerts that have been migrated + */ +export const NUMBER_OF_MIGRATED_ALERTS = 1; + +/** + * The advanced settings config name for the email address + */ +export const MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS = 'monitoring:alertingEmailAddress'; + +export const ALERT_EMAIL_SERVICES = ['gmail', 'hotmail', 'icloud', 'outlook365', 'ses', 'yahoo']; diff --git a/x-pack/legacy/plugins/monitoring/deprecations.js b/x-pack/legacy/plugins/monitoring/deprecations.js index 6e35e86dd9d71..ae8650fd3b26a 100644 --- a/x-pack/legacy/plugins/monitoring/deprecations.js +++ b/x-pack/legacy/plugins/monitoring/deprecations.js @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY } from './common/constants'; +import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY, KIBANA_ALERTING_ENABLED } from './common/constants'; /** * Re-writes deprecated user-defined config settings and logs warnings as a @@ -21,10 +21,20 @@ export const deprecations = () => { const clusterAlertsEnabled = get(settings, 'cluster_alerts.enabled'); const emailNotificationsEnabled = clusterAlertsEnabled && get(settings, 'cluster_alerts.email_notifications.enabled'); - if (emailNotificationsEnabled && !get(settings, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { - log( - `Config key "${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}" will be required for email notifications to work in 7.0."` - ); + if (emailNotificationsEnabled) { + if (KIBANA_ALERTING_ENABLED) { + if (get(settings, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { + log( + `Config key "${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}" is deprecated. Please configure the email adddress through the Stack Monitoring UI instead."` + ); + } + } else { + if (!get(settings, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { + log( + `Config key "${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}" will be required for email notifications to work in 7.0."` + ); + } + } } }, (settings, log) => { diff --git a/x-pack/legacy/plugins/monitoring/index.js b/x-pack/legacy/plugins/monitoring/index.js index ca595836133c2..ade172f527dab 100644 --- a/x-pack/legacy/plugins/monitoring/index.js +++ b/x-pack/legacy/plugins/monitoring/index.js @@ -10,15 +10,20 @@ import { deprecations } from './deprecations'; import { getUiExports } from './ui_exports'; import { Plugin } from './server/plugin'; import { initInfraSource } from './server/lib/logs/init_infra_source'; +import { KIBANA_ALERTING_ENABLED } from './common/constants'; /** * Invokes plugin modules to instantiate the Monitoring plugin for Kibana * @param kibana {Object} Kibana plugin instance * @return {Object} Monitoring UI Kibana plugin object */ +const deps = ['kibana', 'elasticsearch', 'xpack_main']; +if (KIBANA_ALERTING_ENABLED) { + deps.push(...['alerting', 'actions']); +} export const monitoring = kibana => new kibana.Plugin({ - require: ['kibana', 'elasticsearch', 'xpack_main'], + require: deps, id: 'monitoring', configPrefix: 'monitoring', publicDir: resolve(__dirname, 'public'), @@ -59,6 +64,7 @@ export const monitoring = kibana => }), injectUiAppVars: server.injectUiAppVars, log: (...args) => server.log(...args), + logger: server.newPlatform.coreContext.logger, getOSInfo: server.getOSInfo, events: { on: (...args) => server.events.on(...args), @@ -73,11 +79,13 @@ export const monitoring = kibana => xpack_main: server.plugins.xpack_main, elasticsearch: server.plugins.elasticsearch, infra: server.plugins.infra, + alerting: server.plugins.alerting, usageCollection, licensing, }; - new Plugin().setup(serverFacade, plugins); + const plugin = new Plugin(); + plugin.setup(serverFacade, plugins); }, config, deprecations, diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap new file mode 100644 index 0000000000000..4cf1f4df2eb2e --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Status should render a flyout when clicking the link 1`] = ` +<EuiFlyout + aria-labelledby="flyoutTitle" + closeButtonAriaLabel="Closes this dialog" + hideCloseButton={false} + maxWidth={false} + onClose={[Function]} + ownFocus={false} + size="m" +> + <EuiFlyoutHeader + hasBorder={true} + > + <EuiTitle + size="m" + > + <h2> + Monitoring alerts + </h2> + </EuiTitle> + <EuiText> + <p> + Configure an email server and email address to receive alerts. + </p> + </EuiText> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <AlertsConfiguration + emailAddress="test@elastic.co" + onDone={[Function]} + /> + </EuiFlyoutBody> +</EuiFlyout> +`; + +exports[`Status should render a success message if all alerts have been migrated and in setup mode 1`] = ` +<EuiCallOut + color="success" + iconType="flag" + title="Kibana alerting is up to date!" +> + <p> + <EuiLink + onClick={[Function]} + > + Want to make changes? Click here. + </EuiLink> + </p> +</EuiCallOut> +`; + +exports[`Status should render without setup mode 1`] = ` +<Fragment> + <EuiCallOut + color="warning" + title="Hey! We made alerting better!" + > + <p> + <EuiLink + onClick={[Function]} + > + Migrate cluster alerts to our new alerting platform. + </EuiLink> + </p> + </EuiCallOut> + <EuiSpacer /> +</Fragment> +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap new file mode 100644 index 0000000000000..429d19fbb887e --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap @@ -0,0 +1,121 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Configuration shallow view should render step 1 1`] = ` +<Fragment> + <EuiSuperSelect + compressed={false} + fullWidth={false} + hasDividers={true} + isInvalid={false} + isLoading={false} + onChange={[Function]} + options={ + Array [ + Object { + "dropdownDisplay": <EuiText> + Create new email action... + </EuiText>, + "inputDisplay": <EuiText> + Create new email action... + </EuiText>, + "value": "__new__", + }, + ] + } + valueOfSelected="" + /> +</Fragment> +`; + +exports[`Configuration shallow view should render step 2 1`] = ` +<EuiForm + isInvalid={false} +> + <EuiFormRow + describedByIds={Array []} + display="row" + error={null} + fullWidth={false} + hasChildLabel={true} + hasEmptyLabelSpace={false} + isInvalid={false} + label="Email address" + labelType="label" + > + <EuiFieldText + disabled={true} + onChange={[Function]} + value="test@elastic.co" + /> + </EuiFormRow> +</EuiForm> +`; + +exports[`Configuration shallow view should render step 3 1`] = ` +<Fragment> + <EuiButton + isDisabled={true} + isLoading={false} + onClick={[Function]} + > + Save + </EuiButton> +</Fragment> +`; + +exports[`Configuration should render high level steps 1`] = ` +<div + className="euiSteps" +> + <EuiStep + headingElement="p" + key="0" + status="incomplete" + step={1} + title="Create email action" + > + <Step1 + editAction={null} + emailActions={Array []} + emailAddress="test@elastic.co" + onActionDone={[Function]} + selectedEmailActionId="" + setEditAction={[Function]} + setSelectedEmailActionId={[Function]} + /> + </EuiStep> + <EuiStep + headingElement="p" + key="1" + status="disabled" + step={2} + title="Set the email to receive alerts" + > + <Step2 + emailAddress="test@elastic.co" + formErrors={ + Object { + "email": null, + } + } + isDisabled={true} + setEmailAddress={[Function]} + showFormErrors={false} + /> + </EuiStep> + <EuiStep + headingElement="p" + key="2" + status="disabled" + step={3} + title="Confirm and save" + > + <Step3 + error="" + isDisabled={true} + isSaving={false} + save={[Function]} + /> + </EuiStep> +</div> +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap new file mode 100644 index 0000000000000..94d951a94fe29 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap @@ -0,0 +1,300 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step1 creating should render a create form 1`] = ` +<Fragment> + <EuiPanel> + <ManageEmailAction + createEmailAction={[Function]} + isNew={true} + /> + </EuiPanel> +</Fragment> +`; + +exports[`Step1 editing should allow for editing 1`] = ` +<Fragment> + <EuiText> + <p> + Edit the action below. + </p> + </EuiText> + <EuiSpacer /> + <ManageEmailAction + action={ + Object { + "actionTypeId": "1abc", + "config": Object {}, + "id": "1", + "name": "Testing", + } + } + cancel={[Function]} + createEmailAction={[Function]} + isNew={false} + /> +</Fragment> +`; + +exports[`Step1 should render normally 1`] = ` +<Fragment> + <EuiSuperSelect + compressed={false} + fullWidth={false} + hasDividers={true} + isInvalid={false} + isLoading={false} + onChange={[Function]} + options={ + Array [ + Object { + "dropdownDisplay": <EuiText> + From: , Service: + </EuiText>, + "inputDisplay": <EuiText> + From: , Service: + </EuiText>, + "value": "1", + }, + Object { + "dropdownDisplay": <EuiText> + Create new email action... + </EuiText>, + "inputDisplay": <EuiText> + Create new email action... + </EuiText>, + "value": "__new__", + }, + ] + } + valueOfSelected="1" + /> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem + grow={false} + > + <EuiButton + iconType="pencil" + onClick={[Function]} + size="s" + > + Edit + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButton + iconType="play" + isDisabled={false} + isLoading={false} + onClick={[Function]} + size="s" + > + Test + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButton + color="danger" + iconType="trash" + isLoading={false} + onClick={[Function]} + size="s" + > + Delete + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> +</Fragment> +`; + +exports[`Step1 testing should should a tooltip if there is no email address 1`] = ` +<EuiToolTip + content="Please configure an email address below to test this action." + delay="regular" + position="top" +> + <EuiButton + iconType="play" + isDisabled={true} + isLoading={false} + onClick={[Function]} + size="s" + > + Test + </EuiButton> +</EuiToolTip> +`; + +exports[`Step1 testing should show a failed test error 1`] = ` +<Fragment> + <EuiSuperSelect + compressed={false} + fullWidth={false} + hasDividers={true} + isInvalid={false} + isLoading={false} + onChange={[Function]} + options={ + Array [ + Object { + "dropdownDisplay": <EuiText> + From: , Service: + </EuiText>, + "inputDisplay": <EuiText> + From: , Service: + </EuiText>, + "value": "1", + }, + Object { + "dropdownDisplay": <EuiText> + Create new email action... + </EuiText>, + "inputDisplay": <EuiText> + Create new email action... + </EuiText>, + "value": "__new__", + }, + ] + } + valueOfSelected="1" + /> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem + grow={false} + > + <EuiButton + iconType="pencil" + onClick={[Function]} + size="s" + > + Edit + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButton + iconType="play" + isDisabled={false} + isLoading={false} + onClick={[Function]} + size="s" + > + Test + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButton + color="danger" + iconType="trash" + isLoading={false} + onClick={[Function]} + size="s" + > + Delete + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer /> + <EuiCallOut + color="danger" + iconType="alert" + title="Unable to send test email. Please double check your email configuration." + > + <p> + Very detailed error message + </p> + </EuiCallOut> +</Fragment> +`; + +exports[`Step1 testing should show a successful test 1`] = ` +<Fragment> + <EuiSuperSelect + compressed={false} + fullWidth={false} + hasDividers={true} + isInvalid={false} + isLoading={false} + onChange={[Function]} + options={ + Array [ + Object { + "dropdownDisplay": <EuiText> + From: , Service: + </EuiText>, + "inputDisplay": <EuiText> + From: , Service: + </EuiText>, + "value": "1", + }, + Object { + "dropdownDisplay": <EuiText> + Create new email action... + </EuiText>, + "inputDisplay": <EuiText> + Create new email action... + </EuiText>, + "value": "__new__", + }, + ] + } + valueOfSelected="1" + /> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem + grow={false} + > + <EuiButton + iconType="pencil" + onClick={[Function]} + size="s" + > + Edit + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButton + iconType="play" + isDisabled={false} + isLoading={false} + onClick={[Function]} + size="s" + > + Test + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButton + color="danger" + iconType="trash" + isLoading={false} + onClick={[Function]} + size="s" + > + Delete + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer /> + <EuiText + color="secondary" + > + <p> + Looks good on our end! + </p> + </EuiText> +</Fragment> +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap new file mode 100644 index 0000000000000..bac183618b491 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step2 should render normally 1`] = ` +<EuiForm + isInvalid={false} +> + <EuiFormRow + describedByIds={Array []} + display="row" + error={null} + fullWidth={false} + hasChildLabel={true} + hasEmptyLabelSpace={false} + isInvalid={false} + label="Email address" + labelType="label" + > + <EuiFieldText + disabled={false} + onChange={[Function]} + value="test@test.com" + /> + </EuiFormRow> +</EuiForm> +`; + +exports[`Step2 should show form errors 1`] = ` +<EuiForm + isInvalid={true} +> + <EuiFormRow + describedByIds={Array []} + display="row" + error="This is required" + fullWidth={false} + hasChildLabel={true} + hasEmptyLabelSpace={false} + isInvalid={true} + label="Email address" + labelType="label" + > + <EuiFieldText + disabled={false} + onChange={[Function]} + value="test@test.com" + /> + </EuiFormRow> +</EuiForm> +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap new file mode 100644 index 0000000000000..ed15ae9a9cff7 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step3 should render normally 1`] = ` +<Fragment> + <EuiButton + isDisabled={false} + isLoading={false} + onClick={[MockFunction]} + > + Save + </EuiButton> +</Fragment> +`; + +exports[`Step3 should show a disabled state 1`] = ` +<Fragment> + <EuiButton + isDisabled={true} + isLoading={false} + onClick={ + [MockFunction] { + "calls": Array [ + Array [], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + } + } + > + Save + </EuiButton> +</Fragment> +`; + +exports[`Step3 should show a saving state 1`] = ` +<Fragment> + <EuiButton + isDisabled={false} + isLoading={true} + onClick={ + [MockFunction] { + "calls": Array [ + Array [], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + } + } + > + Save + </EuiButton> +</Fragment> +`; + +exports[`Step3 should show an error 1`] = ` +<Fragment> + <EuiCallOut + color="danger" + iconType="alert" + title="Unable to save" + > + <p> + Test error + </p> + </EuiCallOut> + <EuiSpacer /> + <EuiButton + isDisabled={false} + isLoading={false} + onClick={ + [MockFunction] { + "calls": Array [ + Array [], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + } + } + > + Save + </EuiButton> +</Fragment> +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx new file mode 100644 index 0000000000000..6b7e2391e0301 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mockUseEffects } from '../../../jest.helpers'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { kfetch } from 'ui/kfetch'; +import { AlertsConfiguration, AlertsConfigurationProps } from './configuration'; + +jest.mock('ui/kfetch', () => ({ + kfetch: jest.fn(), +})); + +const defaultProps: AlertsConfigurationProps = { + emailAddress: 'test@elastic.co', + onDone: jest.fn(), +}; + +describe('Configuration', () => { + it('should render high level steps', () => { + const component = shallow(<AlertsConfiguration {...defaultProps} />); + expect(component.find('EuiSteps').shallow()).toMatchSnapshot(); + }); + + function getStep(component: ShallowWrapper, index: number) { + return component + .find('EuiSteps') + .shallow() + .find('EuiStep') + .at(index) + .children() + .shallow(); + } + + describe('shallow view', () => { + it('should render step 1', () => { + const component = shallow(<AlertsConfiguration {...defaultProps} />); + const stepOne = getStep(component, 0); + expect(stepOne).toMatchSnapshot(); + }); + + it('should render step 2', () => { + const component = shallow(<AlertsConfiguration {...defaultProps} />); + const stepTwo = getStep(component, 1); + expect(stepTwo).toMatchSnapshot(); + }); + + it('should render step 3', () => { + const component = shallow(<AlertsConfiguration {...defaultProps} />); + const stepThree = getStep(component, 2); + expect(stepThree).toMatchSnapshot(); + }); + }); + + describe('selected action', () => { + const actionId = 'a123b'; + let component: ShallowWrapper; + beforeEach(async () => { + mockUseEffects(2); + + (kfetch as jest.Mock).mockImplementation(() => { + return { + data: [ + { + actionTypeId: '.email', + id: actionId, + config: {}, + }, + ], + }; + }); + + component = shallow(<AlertsConfiguration {...defaultProps} />); + }); + + it('reflect in Step1', async () => { + const steps = component.find('EuiSteps').dive(); + expect( + steps + .find('EuiStep') + .at(0) + .prop('title') + ).toBe('Select email action'); + expect(steps.find('Step1').prop('selectedEmailActionId')).toBe(actionId); + }); + + it('should enable Step2', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step2').prop('isDisabled')).toBe(false); + }); + + it('should enable Step3', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step3').prop('isDisabled')).toBe(false); + }); + }); + + describe('edit action', () => { + let component: ShallowWrapper; + beforeEach(async () => { + (kfetch as jest.Mock).mockImplementation(() => { + return { + data: [], + }; + }); + + component = shallow(<AlertsConfiguration {...defaultProps} />); + }); + + it('disable Step2', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step2').prop('isDisabled')).toBe(true); + }); + + it('disable Step3', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step3').prop('isDisabled')).toBe(true); + }); + }); + + describe('no email address', () => { + let component: ShallowWrapper; + beforeEach(async () => { + (kfetch as jest.Mock).mockImplementation(() => { + return { + data: [ + { + actionTypeId: '.email', + id: 'actionId', + config: {}, + }, + ], + }; + }); + + component = shallow(<AlertsConfiguration {...defaultProps} emailAddress="" />); + }); + + it('should disable Step3', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step3').prop('isDisabled')).toBe(true); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx new file mode 100644 index 0000000000000..0933cd22db7c9 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ReactNode } from 'react'; +import { kfetch } from 'ui/kfetch'; +import { EuiSteps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionResult } from '../../../../../../../plugins/actions/common'; +import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; +import { getMissingFieldErrors } from '../../../lib/form_validation'; +import { Step1 } from './step1'; +import { Step2 } from './step2'; +import { Step3 } from './step3'; + +export interface AlertsConfigurationProps { + emailAddress: string; + onDone: Function; +} + +export interface StepResult { + title: string; + children: ReactNode; + status: any; +} + +export interface AlertsConfigurationForm { + email: string | null; +} + +export const NEW_ACTION_ID = '__new__'; + +export const AlertsConfiguration: React.FC<AlertsConfigurationProps> = ( + props: AlertsConfigurationProps +) => { + const { onDone } = props; + + const [emailActions, setEmailActions] = React.useState<ActionResult[]>([]); + const [selectedEmailActionId, setSelectedEmailActionId] = React.useState(''); + const [editAction, setEditAction] = React.useState<ActionResult | null>(null); + const [emailAddress, setEmailAddress] = React.useState(props.emailAddress); + const [formErrors, setFormErrors] = React.useState<AlertsConfigurationForm>({ email: null }); + const [showFormErrors, setShowFormErrors] = React.useState(false); + const [isSaving, setIsSaving] = React.useState(false); + const [saveError, setSaveError] = React.useState(''); + + React.useEffect(() => { + async function fetchData() { + await fetchEmailActions(); + } + + fetchData(); + }, []); + + React.useEffect(() => { + setFormErrors(getMissingFieldErrors({ email: emailAddress }, { email: '' })); + }, [emailAddress]); + + async function fetchEmailActions() { + const kibanaActions = await kfetch({ + method: 'GET', + pathname: `/api/action/_find`, + }); + + const actions = kibanaActions.data.filter( + (action: ActionResult) => action.actionTypeId === ALERT_ACTION_TYPE_EMAIL + ); + if (actions.length > 0) { + setSelectedEmailActionId(actions[0].id); + } else { + setSelectedEmailActionId(NEW_ACTION_ID); + } + setEmailActions(actions); + } + + async function save() { + if (emailAddress.length === 0) { + setShowFormErrors(true); + return; + } + setIsSaving(true); + setShowFormErrors(false); + + try { + await kfetch({ + method: 'POST', + pathname: `/api/monitoring/v1/alerts`, + body: JSON.stringify({ selectedEmailActionId, emailAddress }), + }); + } catch (err) { + setIsSaving(false); + setSaveError( + err?.body?.message || + i18n.translate('xpack.monitoring.alerts.configuration.unknownError', { + defaultMessage: 'Something went wrong. Please consult the server logs.', + }) + ); + return; + } + + onDone(); + } + + function isStep2Disabled() { + return isStep2AndStep3Disabled(); + } + + function isStep3Disabled() { + return isStep2AndStep3Disabled() || !emailAddress || emailAddress.length === 0; + } + + function isStep2AndStep3Disabled() { + return !!editAction || !selectedEmailActionId || selectedEmailActionId === NEW_ACTION_ID; + } + + function getStep2Status() { + const isDisabled = isStep2AndStep3Disabled(); + + if (isDisabled) { + return 'disabled' as const; + } + + if (emailAddress && emailAddress.length) { + return 'complete' as const; + } + + return 'incomplete' as const; + } + + function getStep1Status() { + if (editAction) { + return 'incomplete' as const; + } + + return selectedEmailActionId ? ('complete' as const) : ('incomplete' as const); + } + + const steps = [ + { + title: emailActions.length + ? i18n.translate('xpack.monitoring.alerts.configuration.selectEmailAction', { + defaultMessage: 'Select email action', + }) + : i18n.translate('xpack.monitoring.alerts.configuration.createEmailAction', { + defaultMessage: 'Create email action', + }), + children: ( + <Step1 + onActionDone={async () => await fetchEmailActions()} + emailActions={emailActions} + selectedEmailActionId={selectedEmailActionId} + setSelectedEmailActionId={setSelectedEmailActionId} + emailAddress={emailAddress} + editAction={editAction} + setEditAction={setEditAction} + /> + ), + status: getStep1Status(), + }, + { + title: i18n.translate('xpack.monitoring.alerts.configuration.setEmailAddress', { + defaultMessage: 'Set the email to receive alerts', + }), + status: getStep2Status(), + children: ( + <Step2 + emailAddress={emailAddress} + setEmailAddress={setEmailAddress} + showFormErrors={showFormErrors} + formErrors={formErrors} + isDisabled={isStep2Disabled()} + /> + ), + }, + { + title: i18n.translate('xpack.monitoring.alerts.configuration.confirm', { + defaultMessage: 'Confirm and save', + }), + status: getStep2Status(), + children: ( + <Step3 isSaving={isSaving} save={save} isDisabled={isStep3Disabled()} error={saveError} /> + ), + }, + ]; + + return ( + <div> + <EuiSteps steps={steps} /> + </div> + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts new file mode 100644 index 0000000000000..7a96c6e324ab3 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AlertsConfiguration } from './configuration'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx new file mode 100644 index 0000000000000..650294c29e9a5 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx @@ -0,0 +1,338 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { omit, pick } from 'lodash'; +import '../../../jest.helpers'; +import { shallow } from 'enzyme'; +import { GetStep1Props } from './step1'; +import { EmailActionData } from '../manage_email_action'; +import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; + +let Step1: React.FC<GetStep1Props>; +let NEW_ACTION_ID: string; + +function setModules() { + Step1 = require('./step1').Step1; + NEW_ACTION_ID = require('./configuration').NEW_ACTION_ID; +} + +describe('Step1', () => { + const emailActions = [ + { + id: '1', + actionTypeId: '1abc', + name: 'Testing', + config: {}, + }, + ]; + const selectedEmailActionId = emailActions[0].id; + const setSelectedEmailActionId = jest.fn(); + const emailAddress = 'test@test.com'; + const editAction = null; + const setEditAction = jest.fn(); + const onActionDone = jest.fn(); + + const defaultProps: GetStep1Props = { + onActionDone, + emailActions, + selectedEmailActionId, + setSelectedEmailActionId, + emailAddress, + editAction, + setEditAction, + }; + + beforeEach(() => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: () => { + return {}; + }, + })); + setModules(); + }); + }); + + it('should render normally', () => { + const component = shallow(<Step1 {...defaultProps} />); + + expect(component).toMatchSnapshot(); + }); + + describe('creating', () => { + it('should render a create form', () => { + const customProps = { + emailActions: [], + selectedEmailActionId: NEW_ACTION_ID, + }; + + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + + expect(component).toMatchSnapshot(); + }); + + it('should render the select box if at least one action exists', () => { + const customProps = { + emailActions: [ + { + id: 'foo', + actionTypeId: '.email', + name: '', + config: {}, + }, + ], + selectedEmailActionId: NEW_ACTION_ID, + }; + + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + expect(component.find('EuiSuperSelect').exists()).toBe(true); + }); + + it('should send up the create to the server', async () => { + const kfetch = jest.fn().mockImplementation(() => {}); + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch, + })); + setModules(); + }); + + const customProps = { + emailActions: [], + selectedEmailActionId: NEW_ACTION_ID, + }; + + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + + const data: EmailActionData = { + service: 'gmail', + host: 'smtp.gmail.com', + port: 465, + secure: true, + from: 'test@test.com', + user: 'user@user.com', + password: 'password', + }; + + const createEmailAction: (data: EmailActionData) => void = component + .find('ManageEmailAction') + .prop('createEmailAction'); + createEmailAction(data); + + expect(kfetch).toHaveBeenCalledWith({ + method: 'POST', + pathname: `/api/action`, + body: JSON.stringify({ + name: 'Email action for Stack Monitoring alerts', + actionTypeId: ALERT_ACTION_TYPE_EMAIL, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + }); + }); + + describe('editing', () => { + it('should allow for editing', () => { + const customProps = { + editAction: emailActions[0], + }; + + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + + expect(component).toMatchSnapshot(); + }); + + it('should send up the edit to the server', async () => { + const kfetch = jest.fn().mockImplementation(() => {}); + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch, + })); + setModules(); + }); + + const customProps = { + editAction: emailActions[0], + }; + + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + + const data: EmailActionData = { + service: 'gmail', + host: 'smtp.gmail.com', + port: 465, + secure: true, + from: 'test@test.com', + user: 'user@user.com', + password: 'password', + }; + + const createEmailAction: (data: EmailActionData) => void = component + .find('ManageEmailAction') + .prop('createEmailAction'); + createEmailAction(data); + + expect(kfetch).toHaveBeenCalledWith({ + method: 'PUT', + pathname: `/api/action/${emailActions[0].id}`, + body: JSON.stringify({ + name: emailActions[0].name, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + }); + }); + + describe('testing', () => { + it('should allow for testing', async () => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: jest.fn().mockImplementation(arg => { + if (arg.pathname === '/api/action/1/_execute') { + return { status: 'ok' }; + } + return {}; + }), + })); + setModules(); + }); + + const component = shallow(<Step1 {...defaultProps} />); + + expect( + component + .find('EuiButton') + .at(1) + .prop('isLoading') + ).toBe(false); + component + .find('EuiButton') + .at(1) + .simulate('click'); + expect( + component + .find('EuiButton') + .at(1) + .prop('isLoading') + ).toBe(true); + await component.update(); + expect( + component + .find('EuiButton') + .at(1) + .prop('isLoading') + ).toBe(false); + }); + + it('should show a successful test', async () => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: (arg: any) => { + if (arg.pathname === '/api/action/1/_execute') { + return { status: 'ok' }; + } + return {}; + }, + })); + setModules(); + }); + + const component = shallow(<Step1 {...defaultProps} />); + + component + .find('EuiButton') + .at(1) + .simulate('click'); + await component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should show a failed test error', async () => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: (arg: any) => { + if (arg.pathname === '/api/action/1/_execute') { + return { message: 'Very detailed error message' }; + } + return {}; + }, + })); + setModules(); + }); + + const component = shallow(<Step1 {...defaultProps} />); + + component + .find('EuiButton') + .at(1) + .simulate('click'); + await component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should not allow testing if there is no email address', () => { + const customProps = { + emailAddress: '', + }; + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + expect( + component + .find('EuiButton') + .at(1) + .prop('isDisabled') + ).toBe(true); + }); + + it('should should a tooltip if there is no email address', () => { + const customProps = { + emailAddress: '', + }; + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + expect(component.find('EuiToolTip')).toMatchSnapshot(); + }); + }); + + describe('deleting', () => { + it('should send up the delete to the server', async () => { + const kfetch = jest.fn().mockImplementation(() => {}); + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch, + })); + setModules(); + }); + + const customProps = { + setSelectedEmailActionId: jest.fn(), + onActionDone: jest.fn(), + }; + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + + await component + .find('EuiButton') + .at(2) + .simulate('click'); + await component.update(); + + expect(kfetch).toHaveBeenCalledWith({ + method: 'DELETE', + pathname: `/api/action/${emailActions[0].id}`, + }); + + expect(customProps.setSelectedEmailActionId).toHaveBeenCalledWith(''); + expect(customProps.onActionDone).toHaveBeenCalled(); + expect( + component + .find('EuiButton') + .at(2) + .prop('isLoading') + ).toBe(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx new file mode 100644 index 0000000000000..fc051a68e29f3 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx @@ -0,0 +1,334 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { + EuiText, + EuiSpacer, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiSuperSelect, + EuiToolTip, + EuiCallOut, +} from '@elastic/eui'; +import { kfetch } from 'ui/kfetch'; +import { omit, pick } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { ActionResult } from '../../../../../../../plugins/actions/common'; +import { ManageEmailAction, EmailActionData } from '../manage_email_action'; +import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; +import { NEW_ACTION_ID } from './configuration'; + +export interface GetStep1Props { + onActionDone: () => Promise<void>; + emailActions: ActionResult[]; + selectedEmailActionId: string; + setSelectedEmailActionId: (id: string) => void; + emailAddress: string; + editAction: ActionResult | null; + setEditAction: (action: ActionResult | null) => void; +} + +export const Step1: React.FC<GetStep1Props> = (props: GetStep1Props) => { + const [isTesting, setIsTesting] = React.useState(false); + const [isDeleting, setIsDeleting] = React.useState(false); + const [testingStatus, setTestingStatus] = React.useState<string | boolean | null>(null); + const [fullTestingError, setFullTestingError] = React.useState(''); + + async function createEmailAction(data: EmailActionData) { + if (props.editAction) { + await kfetch({ + method: 'PUT', + pathname: `/api/action/${props.editAction.id}`, + body: JSON.stringify({ + name: props.editAction.name, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + props.setEditAction(null); + } else { + await kfetch({ + method: 'POST', + pathname: '/api/action', + body: JSON.stringify({ + name: i18n.translate('xpack.monitoring.alerts.configuration.emailAction.name', { + defaultMessage: 'Email action for Stack Monitoring alerts', + }), + actionTypeId: ALERT_ACTION_TYPE_EMAIL, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + } + + await props.onActionDone(); + } + + async function deleteEmailAction(id: string) { + setIsDeleting(true); + + await kfetch({ + method: 'DELETE', + pathname: `/api/action/${id}`, + }); + + if (props.editAction && props.editAction.id === id) { + props.setEditAction(null); + } + if (props.selectedEmailActionId === id) { + props.setSelectedEmailActionId(''); + } + await props.onActionDone(); + setIsDeleting(false); + setTestingStatus(null); + } + + async function testEmailAction() { + setIsTesting(true); + setTestingStatus(null); + + const params = { + subject: 'Kibana alerting test configuration', + message: `This is a test for the configured email action for Kibana alerting.`, + to: [props.emailAddress], + }; + + const result = await kfetch({ + method: 'POST', + pathname: `/api/action/${props.selectedEmailActionId}/_execute`, + body: JSON.stringify({ params }), + }); + if (result.status === 'ok') { + setTestingStatus(true); + } else { + setTestingStatus(false); + setFullTestingError(result.message); + } + setIsTesting(false); + } + + function getTestButton() { + const isTestingDisabled = !props.emailAddress || props.emailAddress.length === 0; + const testBtn = ( + <EuiButton + size="s" + iconType="play" + onClick={testEmailAction} + isLoading={isTesting} + isDisabled={isTestingDisabled} + > + {i18n.translate('xpack.monitoring.alerts.configuration.testConfiguration.buttonText', { + defaultMessage: 'Test', + })} + </EuiButton> + ); + + if (isTestingDisabled) { + return ( + <EuiToolTip + position="top" + content={i18n.translate( + 'xpack.monitoring.alerts.configuration.testConfiguration.disabledTooltipText', + { + defaultMessage: 'Please configure an email address below to test this action.', + } + )} + > + {testBtn} + </EuiToolTip> + ); + } + + return testBtn; + } + + if (props.editAction) { + return ( + <Fragment> + <EuiText> + <p> + {i18n.translate('xpack.monitoring.alerts.configuration.step1.editAction', { + defaultMessage: 'Edit the action below.', + })} + </p> + </EuiText> + <EuiSpacer /> + <ManageEmailAction + createEmailAction={async (data: EmailActionData) => await createEmailAction(data)} + cancel={() => props.setEditAction(null)} + isNew={false} + action={props.editAction} + /> + </Fragment> + ); + } + + const newAction = ( + <EuiText> + {i18n.translate('xpack.monitoring.alerts.configuration.newActionDropdownDisplay', { + defaultMessage: 'Create new email action...', + })} + </EuiText> + ); + + const options = [ + ...props.emailActions.map(action => { + const actionLabel = i18n.translate( + 'xpack.monitoring.alerts.configuration.selectAction.inputDisplay', + { + defaultMessage: 'From: {from}, Service: {service}', + values: { + service: action.config.service, + from: action.config.from, + }, + } + ); + + return { + value: action.id, + inputDisplay: <EuiText>{actionLabel}</EuiText>, + dropdownDisplay: <EuiText>{actionLabel}</EuiText>, + }; + }), + { + value: NEW_ACTION_ID, + inputDisplay: newAction, + dropdownDisplay: newAction, + }, + ]; + + let selectBox: React.ReactNode | null = ( + <EuiSuperSelect + options={options} + valueOfSelected={props.selectedEmailActionId} + onChange={id => props.setSelectedEmailActionId(id)} + hasDividers + /> + ); + let createNew = null; + if (props.selectedEmailActionId === NEW_ACTION_ID) { + createNew = ( + <EuiPanel> + <ManageEmailAction + createEmailAction={async (data: EmailActionData) => await createEmailAction(data)} + isNew={true} + /> + </EuiPanel> + ); + + // If there are no actions, do not show the select box as there are no choices + if (props.emailActions.length === 0) { + selectBox = null; + } else { + // Otherwise, add a spacer + selectBox = ( + <Fragment> + {selectBox} + <EuiSpacer /> + </Fragment> + ); + } + } + + let manageConfiguration = null; + const selectedEmailAction = props.emailActions.find( + action => action.id === props.selectedEmailActionId + ); + + if ( + props.selectedEmailActionId !== NEW_ACTION_ID && + props.selectedEmailActionId && + selectedEmailAction + ) { + let testingStatusUi = null; + if (testingStatus === true) { + testingStatusUi = ( + <Fragment> + <EuiSpacer /> + <EuiText color="secondary"> + <p> + {i18n.translate('xpack.monitoring.alerts.configuration.testConfiguration.success', { + defaultMessage: 'Looks good on our end!', + })} + </p> + </EuiText> + </Fragment> + ); + } else if (testingStatus === false) { + testingStatusUi = ( + <Fragment> + <EuiSpacer /> + <EuiCallOut + title={i18n.translate('xpack.monitoring.alerts.configuration.step1.testingError', { + defaultMessage: + 'Unable to send test email. Please double check your email configuration.', + })} + color="danger" + iconType="alert" + > + <p>{fullTestingError}</p> + </EuiCallOut> + </Fragment> + ); + } + + manageConfiguration = ( + <Fragment> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiButton + size="s" + iconType="pencil" + onClick={() => { + const editAction = + props.emailActions.find(action => action.id === props.selectedEmailActionId) || + null; + props.setEditAction(editAction); + }} + > + {i18n.translate( + 'xpack.monitoring.alerts.configuration.editConfiguration.buttonText', + { + defaultMessage: 'Edit', + } + )} + </EuiButton> + </EuiFlexItem> + <EuiFlexItem grow={false}>{getTestButton()}</EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + size="s" + color="danger" + iconType="trash" + onClick={() => deleteEmailAction(props.selectedEmailActionId)} + isLoading={isDeleting} + > + {i18n.translate( + 'xpack.monitoring.alerts.configuration.deleteConfiguration.buttonText', + { + defaultMessage: 'Delete', + } + )} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + {testingStatusUi} + </Fragment> + ); + } + + return ( + <Fragment> + {selectBox} + {manageConfiguration} + {createNew} + </Fragment> + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx new file mode 100644 index 0000000000000..14e3cb078f9cc --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import '../../../jest.helpers'; +import { shallow } from 'enzyme'; +import { Step2, GetStep2Props } from './step2'; + +describe('Step2', () => { + const defaultProps: GetStep2Props = { + emailAddress: 'test@test.com', + setEmailAddress: jest.fn(), + showFormErrors: false, + formErrors: { email: null }, + isDisabled: false, + }; + + it('should render normally', () => { + const component = shallow(<Step2 {...defaultProps} />); + expect(component).toMatchSnapshot(); + }); + + it('should set the email address properly', () => { + const newEmail = 'email@email.com'; + const component = shallow(<Step2 {...defaultProps} />); + component.find('EuiFieldText').simulate('change', { target: { value: newEmail } }); + expect(defaultProps.setEmailAddress).toHaveBeenCalledWith(newEmail); + }); + + it('should show form errors', () => { + const customProps = { + showFormErrors: true, + formErrors: { + email: 'This is required', + }, + }; + const component = shallow(<Step2 {...defaultProps} {...customProps} />); + expect(component).toMatchSnapshot(); + }); + + it('should disable properly', () => { + const customProps = { + isDisabled: true, + }; + const component = shallow(<Step2 {...defaultProps} {...customProps} />); + expect(component.find('EuiFieldText').prop('disabled')).toBe(true); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx new file mode 100644 index 0000000000000..974dd8513d231 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiForm, EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AlertsConfigurationForm } from './configuration'; + +export interface GetStep2Props { + emailAddress: string; + setEmailAddress: (email: string) => void; + showFormErrors: boolean; + formErrors: AlertsConfigurationForm; + isDisabled: boolean; +} + +export const Step2: React.FC<GetStep2Props> = (props: GetStep2Props) => { + return ( + <EuiForm isInvalid={props.showFormErrors}> + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.configuration.emailAddressLabel', { + defaultMessage: 'Email address', + })} + error={props.formErrors.email} + isInvalid={props.showFormErrors && !!props.formErrors.email} + > + <EuiFieldText + value={props.emailAddress} + disabled={props.isDisabled} + onChange={e => props.setEmailAddress(e.target.value)} + /> + </EuiFormRow> + </EuiForm> + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx new file mode 100644 index 0000000000000..9b1304c42a507 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import '../../../jest.helpers'; +import { shallow } from 'enzyme'; +import { Step3 } from './step3'; + +describe('Step3', () => { + const defaultProps = { + isSaving: false, + isDisabled: false, + save: jest.fn(), + error: null, + }; + + it('should render normally', () => { + const component = shallow(<Step3 {...defaultProps} />); + expect(component).toMatchSnapshot(); + }); + + it('should save properly', () => { + const component = shallow(<Step3 {...defaultProps} />); + component.find('EuiButton').simulate('click'); + expect(defaultProps.save).toHaveBeenCalledWith(); + }); + + it('should show a saving state', () => { + const customProps = { isSaving: true }; + const component = shallow(<Step3 {...defaultProps} {...customProps} />); + expect(component).toMatchSnapshot(); + }); + + it('should show a disabled state', () => { + const customProps = { isDisabled: true }; + const component = shallow(<Step3 {...defaultProps} {...customProps} />); + expect(component).toMatchSnapshot(); + }); + + it('should show an error', () => { + const customProps = { error: 'Test error' }; + const component = shallow(<Step3 {...defaultProps} {...customProps} />); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx new file mode 100644 index 0000000000000..80acb8992cbc1 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { EuiButton, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export interface GetStep3Props { + isSaving: boolean; + isDisabled: boolean; + save: () => void; + error: string | null; +} + +export const Step3: React.FC<GetStep3Props> = (props: GetStep3Props) => { + let errorUi = null; + if (props.error) { + errorUi = ( + <Fragment> + <EuiCallOut + title={i18n.translate('xpack.monitoring.alerts.configuration.step3.saveError', { + defaultMessage: 'Unable to save', + })} + color="danger" + iconType="alert" + > + <p>{props.error}</p> + </EuiCallOut> + <EuiSpacer /> + </Fragment> + ); + } + + return ( + <Fragment> + {errorUi} + <EuiButton isLoading={props.isSaving} isDisabled={props.isDisabled} onClick={props.save}> + {i18n.translate('xpack.monitoring.alerts.configuration.save', { + defaultMessage: 'Save', + })} + </EuiButton> + </Fragment> + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx new file mode 100644 index 0000000000000..2bd9804795cb5 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx @@ -0,0 +1,301 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { + EuiForm, + EuiFormRow, + EuiFieldText, + EuiLink, + EuiSpacer, + EuiFieldNumber, + EuiFieldPassword, + EuiSwitch, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiSuperSelect, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionResult } from '../../../../../../plugins/actions/common'; +import { getMissingFieldErrors, hasErrors, getRequiredFieldError } from '../../lib/form_validation'; +import { ALERT_EMAIL_SERVICES } from '../../../common/constants'; + +export interface EmailActionData { + service: string; + host: string; + port?: number; + secure: boolean; + from: string; + user: string; + password: string; +} + +interface ManageActionModalProps { + createEmailAction: (handler: EmailActionData) => void; + cancel?: () => void; + isNew: boolean; + action?: ActionResult | null; +} + +const DEFAULT_DATA: EmailActionData = { + service: '', + host: '', + port: 0, + secure: false, + from: '', + user: '', + password: '', +}; + +const CREATE_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.createLabel', { + defaultMessage: 'Create email action', +}); +const SAVE_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.saveLabel', { + defaultMessage: 'Save email action', +}); +const CANCEL_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.cancelLabel', { + defaultMessage: 'Cancel', +}); + +const NEW_SERVICE_ID = '__new__'; + +export const ManageEmailAction: React.FC<ManageActionModalProps> = ( + props: ManageActionModalProps +) => { + const { createEmailAction, cancel, isNew, action } = props; + + const defaultData = Object.assign({}, DEFAULT_DATA, action ? action.config : {}); + const [isSaving, setIsSaving] = React.useState(false); + const [showErrors, setShowErrors] = React.useState(false); + const [errors, setErrors] = React.useState<EmailActionData | any>( + getMissingFieldErrors(defaultData, DEFAULT_DATA) + ); + const [data, setData] = React.useState(defaultData); + const [createNewService, setCreateNewService] = React.useState(false); + const [newService, setNewService] = React.useState(''); + + React.useEffect(() => { + const missingFieldErrors = getMissingFieldErrors(data, DEFAULT_DATA); + if (!missingFieldErrors.service) { + if (data.service === NEW_SERVICE_ID && !newService) { + missingFieldErrors.service = getRequiredFieldError('service'); + } + } + setErrors(missingFieldErrors); + }, [data, newService]); + + async function saveEmailAction() { + setShowErrors(true); + if (!hasErrors(errors)) { + setShowErrors(false); + setIsSaving(true); + const mergedData = { + ...data, + service: data.service === NEW_SERVICE_ID ? newService : data.service, + }; + try { + await createEmailAction(mergedData); + } catch (err) { + setErrors({ + general: err.body.message, + }); + } + } + } + + const serviceOptions = ALERT_EMAIL_SERVICES.map(service => ({ + value: service, + inputDisplay: <EuiText>{service}</EuiText>, + dropdownDisplay: <EuiText>{service}</EuiText>, + })); + + serviceOptions.push({ + value: NEW_SERVICE_ID, + inputDisplay: ( + <EuiText> + {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.addingNewServiceText', { + defaultMessage: 'Adding new service...', + })} + </EuiText> + ), + dropdownDisplay: ( + <EuiText> + {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.addNewServiceText', { + defaultMessage: 'Add new service...', + })} + </EuiText> + ), + }); + + let addNewServiceUi = null; + if (createNewService) { + addNewServiceUi = ( + <Fragment> + <EuiSpacer /> + <EuiFieldText + value={newService} + onChange={e => setNewService(e.target.value)} + isInvalid={showErrors} + /> + </Fragment> + ); + } + + return ( + <EuiForm isInvalid={showErrors} error={Object.values(errors)}> + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.serviceText', { + defaultMessage: 'Service', + })} + helpText={ + <EuiLink external target="_blank" href="https://nodemailer.com/smtp/well-known/"> + {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.serviceHelpText', { + defaultMessage: 'Find out more', + })} + </EuiLink> + } + error={errors.service} + isInvalid={showErrors && !!errors.service} + > + <Fragment> + <EuiSuperSelect + options={serviceOptions} + valueOfSelected={data.service} + onChange={id => { + if (id === NEW_SERVICE_ID) { + setCreateNewService(true); + setData({ ...data, service: NEW_SERVICE_ID }); + } else { + setCreateNewService(false); + setData({ ...data, service: id }); + } + }} + hasDividers + isInvalid={showErrors && !!errors.service} + /> + {addNewServiceUi} + </Fragment> + </EuiFormRow> + + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.hostText', { + defaultMessage: 'Host', + })} + helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.hostHelpText', { + defaultMessage: 'Host name of the service provider', + })} + error={errors.host} + isInvalid={showErrors && !!errors.host} + > + <EuiFieldText + value={data.host} + onChange={e => setData({ ...data, host: e.target.value })} + isInvalid={showErrors && !!errors.host} + /> + </EuiFormRow> + + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.portText', { + defaultMessage: 'Port', + })} + helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.portHelpText', { + defaultMessage: 'Port number of the service provider', + })} + error={errors.port} + isInvalid={showErrors && !!errors.port} + > + <EuiFieldNumber + value={data.port} + onChange={e => setData({ ...data, port: parseInt(e.target.value, 10) })} + isInvalid={showErrors && !!errors.port} + /> + </EuiFormRow> + + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.secureText', { + defaultMessage: 'Secure', + })} + helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.secureHelpText', { + defaultMessage: 'Whether to use TLS with the service provider', + })} + > + <EuiSwitch + label="" + checked={data.secure} + onChange={e => setData({ ...data, secure: e.target.checked })} + /> + </EuiFormRow> + + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.fromText', { + defaultMessage: 'From', + })} + helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.fromHelpText', { + defaultMessage: 'The from email address for alerts', + })} + error={errors.from} + isInvalid={showErrors && !!errors.from} + > + <EuiFieldText + value={data.from} + onChange={e => setData({ ...data, from: e.target.value })} + isInvalid={showErrors && !!errors.from} + /> + </EuiFormRow> + + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.userText', { + defaultMessage: 'User', + })} + helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.userHelpText', { + defaultMessage: 'The user to use with the service provider', + })} + error={errors.user} + isInvalid={showErrors && !!errors.user} + > + <EuiFieldText + value={data.user} + onChange={e => setData({ ...data, user: e.target.value })} + isInvalid={showErrors && !!errors.user} + /> + </EuiFormRow> + + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.passwordText', { + defaultMessage: 'Password', + })} + helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.passwordHelpText', { + defaultMessage: 'The password to use with the service provider', + })} + error={errors.password} + isInvalid={showErrors && !!errors.password} + > + <EuiFieldPassword + value={data.password} + onChange={e => setData({ ...data, password: e.target.value })} + isInvalid={showErrors && !!errors.password} + /> + </EuiFormRow> + + <EuiSpacer /> + + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiButton type="submit" fill onClick={saveEmailAction} isLoading={isSaving}> + {isNew ? CREATE_LABEL : SAVE_LABEL} + </EuiButton> + </EuiFlexItem> + {!action || isNew ? null : ( + <EuiFlexItem grow={false}> + <EuiButton onClick={cancel}>{CANCEL_LABEL}</EuiButton> + </EuiFlexItem> + )} + </EuiFlexGroup> + </EuiForm> + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx new file mode 100644 index 0000000000000..258a5b68db372 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { kfetch } from 'ui/kfetch'; +import { AlertsStatus, AlertsStatusProps } from './status'; +import { ALERT_TYPE_PREFIX } from '../../../common/constants'; +import { getSetupModeState } from '../../lib/setup_mode'; +import { mockUseEffects } from '../../jest.helpers'; + +jest.mock('../../lib/setup_mode', () => ({ + getSetupModeState: jest.fn(), + addSetupModeCallback: jest.fn(), + toggleSetupMode: jest.fn(), +})); + +jest.mock('ui/kfetch', () => ({ + kfetch: jest.fn(), +})); + +const defaultProps: AlertsStatusProps = { + clusterUuid: '1adsb23', + emailAddress: 'test@elastic.co', +}; + +describe('Status', () => { + beforeEach(() => { + mockUseEffects(2); + + (getSetupModeState as jest.Mock).mockReturnValue({ + enabled: false, + }); + + (kfetch as jest.Mock).mockImplementation(({ pathname }) => { + if (pathname === '/internal/security/api_key/privileges') { + return { areApiKeysEnabled: true }; + } + return { + data: [], + }; + }); + }); + + it('should render without setup mode', () => { + const component = shallow(<AlertsStatus {...defaultProps} />); + expect(component).toMatchSnapshot(); + }); + + it('should render a flyout when clicking the link', async () => { + (getSetupModeState as jest.Mock).mockReturnValue({ + enabled: true, + }); + + const component = shallow(<AlertsStatus {...defaultProps} />); + component.find('EuiLink').simulate('click'); + await component.update(); + expect(component.find('EuiFlyout')).toMatchSnapshot(); + }); + + it('should render a success message if all alerts have been migrated and in setup mode', async () => { + (kfetch as jest.Mock).mockReturnValue({ + data: [ + { + alertTypeId: ALERT_TYPE_PREFIX, + }, + ], + }); + + (getSetupModeState as jest.Mock).mockReturnValue({ + enabled: true, + }); + + const component = shallow(<AlertsStatus {...defaultProps} />); + await component.update(); + expect(component.find('EuiCallOut')).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx new file mode 100644 index 0000000000000..0ee0015ed39a7 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { kfetch } from 'ui/kfetch'; +import { + EuiSpacer, + EuiCallOut, + EuiTitle, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiLink, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { Alert } from '../../../../alerting/server/types'; +import { getSetupModeState, addSetupModeCallback, toggleSetupMode } from '../../lib/setup_mode'; +import { NUMBER_OF_MIGRATED_ALERTS, ALERT_TYPE_PREFIX } from '../../../common/constants'; +import { AlertsConfiguration } from './configuration'; + +export interface AlertsStatusProps { + clusterUuid: string; + emailAddress: string; +} + +export const AlertsStatus: React.FC<AlertsStatusProps> = (props: AlertsStatusProps) => { + const { emailAddress } = props; + + const [setupModeEnabled, setSetupModeEnabled] = React.useState(getSetupModeState().enabled); + const [kibanaAlerts, setKibanaAlerts] = React.useState<Alert[]>([]); + const [showMigrationFlyout, setShowMigrationFlyout] = React.useState(false); + const [isSecurityConfigured, setIsSecurityConfigured] = React.useState(false); + + React.useEffect(() => { + async function fetchAlertsStatus() { + const alerts = await kfetch({ method: 'GET', pathname: `/api/alert/_find` }); + const monitoringAlerts = alerts.data.filter((alert: Alert) => + alert.alertTypeId.startsWith(ALERT_TYPE_PREFIX) + ); + setKibanaAlerts(monitoringAlerts); + } + + fetchAlertsStatus(); + fetchSecurityConfigured(); + }, [setupModeEnabled, showMigrationFlyout]); + + React.useEffect(() => { + if (!setupModeEnabled && showMigrationFlyout) { + setShowMigrationFlyout(false); + } + }, [setupModeEnabled, showMigrationFlyout]); + + async function fetchSecurityConfigured() { + const response = await kfetch({ pathname: '/internal/security/api_key/privileges' }); + setIsSecurityConfigured(response.areApiKeysEnabled); + } + + addSetupModeCallback(() => setSetupModeEnabled(getSetupModeState().enabled)); + + function enterSetupModeAndOpenFlyout() { + toggleSetupMode(true); + setShowMigrationFlyout(true); + } + + function getSecurityConfigurationErrorUi() { + if (isSecurityConfigured) { + return null; + } + + const link = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/security-settings.html#api-key-service-settings`; + return ( + <Fragment> + <EuiSpacer /> + <EuiCallOut + title={i18n.translate( + 'xpack.monitoring.alerts.configuration.securityConfigurationErrorTitle', + { + defaultMessage: 'API keys are not enabled in Elasticsearch', + } + )} + color="danger" + iconType="alert" + > + <p> + <FormattedMessage + id="xpack.monitoring.alerts.configuration.securityConfigurationErrorMessage" + defaultMessage="Refer to the {link} to enable API keys." + values={{ + link: ( + <EuiLink href={link} target="_blank"> + {i18n.translate( + 'xpack.monitoring.alerts.configuration.securityConfigurationError.docsLinkLabel', + { + defaultMessage: 'docs', + } + )} + </EuiLink> + ), + }} + /> + </p> + </EuiCallOut> + </Fragment> + ); + } + + function renderContent() { + let flyout = null; + if (showMigrationFlyout) { + flyout = ( + <EuiFlyout onClose={() => setShowMigrationFlyout(false)} aria-labelledby="flyoutTitle"> + <EuiFlyoutHeader hasBorder> + <EuiTitle size="m"> + <h2> + {i18n.translate('xpack.monitoring.alerts.status.flyoutTitle', { + defaultMessage: 'Monitoring alerts', + })} + </h2> + </EuiTitle> + <EuiText> + <p> + {i18n.translate('xpack.monitoring.alerts.status.flyoutSubtitle', { + defaultMessage: 'Configure an email server and email address to receive alerts.', + })} + </p> + </EuiText> + {getSecurityConfigurationErrorUi()} + </EuiFlyoutHeader> + <EuiFlyoutBody> + <AlertsConfiguration + emailAddress={emailAddress} + onDone={() => setShowMigrationFlyout(false)} + /> + </EuiFlyoutBody> + </EuiFlyout> + ); + } + + const allMigrated = kibanaAlerts.length === NUMBER_OF_MIGRATED_ALERTS; + if (allMigrated) { + if (setupModeEnabled) { + return ( + <Fragment> + <EuiCallOut + color="success" + title={i18n.translate('xpack.monitoring.alerts.status.upToDate', { + defaultMessage: 'Kibana alerting is up to date!', + })} + iconType="flag" + > + <p> + <EuiLink onClick={enterSetupModeAndOpenFlyout}> + {i18n.translate('xpack.monitoring.alerts.status.manage', { + defaultMessage: 'Want to make changes? Click here.', + })} + </EuiLink> + </p> + </EuiCallOut> + {flyout} + </Fragment> + ); + } + } else { + return ( + <Fragment> + <EuiCallOut + color="warning" + title={i18n.translate('xpack.monitoring.alerts.status.needToMigrateTitle', { + defaultMessage: 'Hey! We made alerting better!', + })} + > + <p> + <EuiLink onClick={enterSetupModeAndOpenFlyout}> + {i18n.translate('xpack.monitoring.alerts.status.needToMigrate', { + defaultMessage: 'Migrate cluster alerts to our new alerting platform.', + })} + </EuiLink> + </p> + </EuiCallOut> + {flyout} + </Fragment> + ); + } + } + + const content = renderContent(); + if (content) { + return ( + <Fragment> + {content} + <EuiSpacer /> + </Fragment> + ); + } + + return null; +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js index 33b26c7ec56e0..a8001638f4399 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js @@ -4,11 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Fragment } from 'react'; +import moment from 'moment-timezone'; import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert'; import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; -import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; +import { + CALCULATE_DURATION_SINCE, + KIBANA_ALERTING_ENABLED, + ALERT_TYPE_LICENSE_EXPIRATION, + CALCULATE_DURATION_UNTIL, +} from '../../../../common/constants'; import { formatDateTimeLocal } from '../../../../common/formatting'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -21,6 +27,7 @@ import { EuiText, EuiSpacer, EuiCallOut, + EuiLink, } from '@elastic/eui'; export function AlertsPanel({ alerts, changeUrl }) { @@ -82,9 +89,52 @@ export function AlertsPanel({ alerts, changeUrl }) { ); } - const topAlertItems = alerts.map((item, index) => ( - <TopAlertItem item={item} key={`top-alert-item-${index}`} index={index} /> - )); + const alertsList = KIBANA_ALERTING_ENABLED + ? alerts.map((alert, idx) => { + const callOutProps = mapSeverity(alert.severity); + let message = alert.message + // scan message prefix and replace relative times + // \w: Matches any alphanumeric character from the basic Latin alphabet, including the underscore. Equivalent to [A-Za-z0-9_]. + .replace( + '#relative', + formatTimestampToDuration(alert.expirationTime, CALCULATE_DURATION_UNTIL) + ) + .replace('#absolute', moment.tz(alert.expirationTime, moment.tz.guess()).format('LLL z')); + + if (!alert.isFiring) { + callOutProps.title = i18n.translate( + 'xpack.monitoring.cluster.overview.alertsPanel.severityIconTitle', + { + defaultMessage: '{severityIconTitle} (resolved {time} ago)', + values: { + severityIconTitle: callOutProps.title, + time: formatTimestampToDuration(alert.resolvedMS, CALCULATE_DURATION_SINCE), + }, + } + ); + callOutProps.color = 'success'; + callOutProps.iconType = 'check'; + } else { + if (alert.type === ALERT_TYPE_LICENSE_EXPIRATION) { + message = ( + <Fragment> + {message} +   + <EuiLink href="#license">Please update your license</EuiLink> + </Fragment> + ); + } + } + + return ( + <EuiCallOut key={idx} {...callOutProps}> + <p>{message}</p> + </EuiCallOut> + ); + }) + : alerts.map((item, index) => ( + <TopAlertItem item={item} key={`top-alert-item-${index}`} index={index} /> + )); return ( <div data-test-subj="clusterAlertsContainer"> @@ -109,7 +159,7 @@ export function AlertsPanel({ alerts, changeUrl }) { </EuiFlexItem> </EuiFlexGroup> <EuiSpacer size="m" /> - {topAlertItems} + {alertsList} <EuiSpacer size="xxl" /> </div> ); diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js index cad4bbf411c34..eee51c416d11e 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js @@ -10,15 +10,22 @@ import { KibanaPanel } from './kibana_panel'; import { LogstashPanel } from './logstash_panel'; import { AlertsPanel } from './alerts_panel'; import { BeatsPanel } from './beats_panel'; - import { EuiPage, EuiPageBody, EuiScreenReaderOnly } from '@elastic/eui'; import { ApmPanel } from './apm_panel'; -import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertsStatus } from '../../alerts/status'; +import { + STANDALONE_CLUSTER_CLUSTER_UUID, + KIBANA_ALERTING_ENABLED, +} from '../../../../common/constants'; export function Overview(props) { const isFromStandaloneCluster = props.cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID; + const kibanaAlerts = KIBANA_ALERTING_ENABLED ? ( + <AlertsStatus emailAddress={props.emailAddress} /> + ) : null; + return ( <EuiPage> <EuiPageBody> @@ -30,6 +37,9 @@ export function Overview(props) { /> </h1> </EuiScreenReaderOnly> + + {kibanaAlerts} + <AlertsPanel alerts={props.cluster.alerts} changeUrl={props.changeUrl} /> {!isFromStandaloneCluster ? ( diff --git a/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts b/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts new file mode 100644 index 0000000000000..46ba603d30138 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; + +/** + * Suppress React 16.8 act() warnings globally. + * The react teams fix won't be out of alpha until 16.9.0. + * https://github.com/facebook/react/issues/14769#issuecomment-514589856 + */ +const consoleError = console.error; // eslint-disable-line no-console +beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation((...args) => { + if (!args[0].includes('Warning: An update to %s inside a test was not wrapped in act')) { + consoleError(...args); + } + }); +}); + +export function mockUseEffects(count = 1) { + const spy = jest.spyOn(React, 'useEffect'); + for (let i = 0; i < count; i++) { + spy.mockImplementationOnce(f => f()); + } +} + +// export function mockUseEffectForDeps(deps, count = 1) { +// const spy = jest.spyOn(React, 'useEffect'); +// for (let i = 0; i < count; i++) { +// spy.mockImplementationOnce((f, depList) => { + +// }); +// } +// } diff --git a/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.js b/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx similarity index 94% rename from x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.js rename to x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx index 9a51a88596926..22ce32103c208 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx @@ -7,12 +7,13 @@ import React from 'react'; import { contains } from 'lodash'; import { toastNotifications } from 'ui/notify'; +// @ts-ignore import { formatMsg } from 'ui/notify/lib'; import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; -export function formatMonitoringError(err) { +export function formatMonitoringError(err: any) { // TODO: We should stop using Boom for errors and instead write a custom handler to return richer error objects // then we can do better messages, such as highlighting the Cluster UUID instead of requiring it be part of the message if (err.status && err.status !== -1 && err.data) { @@ -33,10 +34,10 @@ export function formatMonitoringError(err) { return formatMsg(err); } -export function ajaxErrorHandlersProvider($injector) { +export function ajaxErrorHandlersProvider($injector: any) { const kbnUrl = $injector.get('kbnUrl'); - return err => { + return (err: any) => { if (err.status === 403) { // redirect to error message view kbnUrl.redirect('access-denied'); diff --git a/x-pack/legacy/plugins/monitoring/public/lib/form_validation.ts b/x-pack/legacy/plugins/monitoring/public/lib/form_validation.ts new file mode 100644 index 0000000000000..98d56f9790be4 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/lib/form_validation.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { isString, isNumber, capitalize } from 'lodash'; + +export function getRequiredFieldError(field: string): string { + return i18n.translate('xpack.monitoring.alerts.migrate.manageAction.requiredFieldError', { + defaultMessage: '{field} is a required field.', + values: { + field: capitalize(field), + }, + }); +} + +export function getMissingFieldErrors(data: any, defaultData: any) { + const errors: any = {}; + + for (const key in data) { + if (!data.hasOwnProperty(key)) { + continue; + } + + if (isString(defaultData[key])) { + if (!data[key] || data[key].length === 0) { + errors[key] = getRequiredFieldError(key); + } + } else if (isNumber(defaultData[key])) { + if (isNaN(data[key]) || data[key] === 0) { + errors[key] = getRequiredFieldError(key); + } + } + } + + return errors; +} + +export function hasErrors(errors: any) { + for (const error in errors) { + if (error.length) { + return true; + } + } + return false; +} diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js index aa931368b34c2..4a2b470f04c72 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js @@ -90,7 +90,7 @@ describe('setup_mode', () => { } catch (err) { error = err; } - expect(error).toEqual( + expect(error.message).toEqual( 'Unable to interact with setup ' + 'mode because the angular injector was not previously set. This needs to be ' + 'set by calling `initSetupModeState`.' @@ -255,9 +255,9 @@ describe('setup_mode', () => { await toggleSetupMode(true); injectorModulesMock.$http.post.mockClear(); await updateSetupModeData(undefined, true); - expect( - injectorModulesMock.$http.post - ).toHaveBeenCalledWith('../api/monitoring/v1/setup/collection/cluster', { ccs: undefined }); + const url = '../api/monitoring/v1/setup/collection/cluster'; + const args = { ccs: undefined }; + expect(injectorModulesMock.$http.post).toHaveBeenCalledWith(url, args); }); }); }); diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx similarity index 76% rename from x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js rename to x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx index 41aae01307617..d805c10247b2e 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx @@ -6,31 +6,49 @@ import React from 'react'; import { render } from 'react-dom'; -import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { get, contains } from 'lodash'; import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; import { npSetup } from 'ui/new_platform'; +import { PluginsSetup } from 'ui/new_platform/new_platform'; +import { CloudSetup } from '../../../../../plugins/cloud/public'; +import { ajaxErrorHandlersProvider } from './ajax_error_handler'; +import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; + +interface PluginsSetupWithCloud extends PluginsSetup { + cloud: CloudSetup; +} -function isOnPage(hash) { +function isOnPage(hash: string) { return contains(window.location.hash, hash); } -const angularState = { +interface IAngularState { + injector: any; + scope: any; +} + +const angularState: IAngularState = { injector: null, scope: null, }; const checkAngularState = () => { if (!angularState.injector || !angularState.scope) { - throw 'Unable to interact with setup mode because the angular injector was not previously set.' + - ' This needs to be set by calling `initSetupModeState`.'; + throw new Error( + 'Unable to interact with setup mode because the angular injector was not previously set.' + + ' This needs to be set by calling `initSetupModeState`.' + ); } }; -const setupModeState = { +interface ISetupModeState { + enabled: boolean; + data: any; + callbacks: Function[]; +} +const setupModeState: ISetupModeState = { enabled: false, data: null, callbacks: [], @@ -38,7 +56,7 @@ const setupModeState = { export const getSetupModeState = () => setupModeState; -export const setNewlyDiscoveredClusterUuid = clusterUuid => { +export const setNewlyDiscoveredClusterUuid = (clusterUuid: string) => { const globalState = angularState.injector.get('globalState'); const executor = angularState.injector.get('$executor'); angularState.scope.$apply(() => { @@ -48,7 +66,7 @@ export const setNewlyDiscoveredClusterUuid = clusterUuid => { executor.run(); }; -export const fetchCollectionData = async (uuid, fetchWithoutClusterUuid = false) => { +export const fetchCollectionData = async (uuid?: string, fetchWithoutClusterUuid = false) => { checkAngularState(); const http = angularState.injector.get('$http'); @@ -75,19 +93,19 @@ export const fetchCollectionData = async (uuid, fetchWithoutClusterUuid = false) } }; -const notifySetupModeDataChange = oldData => { - setupModeState.callbacks.forEach(cb => cb(oldData)); +const notifySetupModeDataChange = (oldData?: any) => { + setupModeState.callbacks.forEach((cb: Function) => cb(oldData)); }; -export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) => { +export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid = false) => { const oldData = setupModeState.data; const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid); setupModeState.data = data; - const { cloud } = npSetup.plugins; + const { cloud } = npSetup.plugins as PluginsSetupWithCloud; const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); const hasPermissions = get(data, '_meta.hasPermissions', false); if (isCloudEnabled || !hasPermissions) { - let text = null; + let text: string = ''; if (!hasPermissions) { text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { defaultMessage: 'You do not have the necessary permissions to do this.', @@ -113,9 +131,9 @@ export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) const globalState = angularState.injector.get('globalState'); const clusterUuid = globalState.cluster_uuid; if (!clusterUuid) { - const liveClusterUuid = get(data, '_meta.liveClusterUuid'); + const liveClusterUuid: string = get(data, '_meta.liveClusterUuid'); const migratedEsNodes = Object.values(get(data, 'elasticsearch.byUuid', {})).filter( - node => node.isPartiallyMigrated || node.isFullyMigrated + (node: any) => node.isPartiallyMigrated || node.isFullyMigrated ); if (liveClusterUuid && migratedEsNodes.length > 0) { setNewlyDiscoveredClusterUuid(liveClusterUuid); @@ -140,7 +158,7 @@ export const disableElasticsearchInternalCollection = async () => { } }; -export const toggleSetupMode = inSetupMode => { +export const toggleSetupMode = (inSetupMode: boolean) => { checkAngularState(); const globalState = angularState.injector.get('globalState'); @@ -164,7 +182,7 @@ export const setSetupModeMenuItem = () => { } const globalState = angularState.injector.get('globalState'); - const { cloud } = npSetup.plugins; + const { cloud } = npSetup.plugins as PluginsSetupWithCloud; const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); const enabled = !globalState.inSetupMode && !isCloudEnabled; @@ -174,10 +192,14 @@ export const setSetupModeMenuItem = () => { ); }; -export const initSetupModeState = async ($scope, $injector, callback) => { +export const addSetupModeCallback = (callback: Function) => setupModeState.callbacks.push(callback); + +export const initSetupModeState = async ($scope: any, $injector: any, callback?: Function) => { angularState.scope = $scope; angularState.injector = $injector; - callback && setupModeState.callbacks.push(callback); + if (callback) { + setupModeState.callbacks.push(callback); + } const globalState = $injector.get('globalState'); if (globalState.inSetupMode) { diff --git a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js b/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js index 57a7850b6fd53..1bfc76b766457 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js @@ -24,7 +24,7 @@ function getPageData($injector) { const globalState = $injector.get('globalState'); const $http = $injector.get('$http'); const Private = $injector.get('Private'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/alerts`; + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/legacy_alerts`; const timeBounds = timefilter.getBounds(); diff --git a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js b/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js index bec90f3230571..e7107860d61fa 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; +import { isEmpty } from 'lodash'; +import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; import uiRoutes from 'ui/routes'; import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; @@ -12,7 +14,11 @@ import { MonitoringViewBaseController } from '../../'; import { Overview } from 'plugins/monitoring/components/cluster/overview'; import { I18nContext } from 'ui/i18n'; import { SetupModeRenderer } from '../../../components/renderers'; -import { CODE_PATH_ALL } from '../../../../common/constants'; +import { + CODE_PATH_ALL, + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, + KIBANA_ALERTING_ENABLED, +} from '../../../../common/constants'; const CODE_PATHS = [CODE_PATH_ALL]; @@ -31,6 +37,7 @@ uiRoutes.when('/overview', { const monitoringClusters = $injector.get('monitoringClusters'); const globalState = $injector.get('globalState'); const showLicenseExpiration = $injector.get('showLicenseExpiration'); + const config = $injector.get('config'); super({ title: i18n.translate('xpack.monitoring.cluster.overviewTitle', { @@ -58,7 +65,16 @@ uiRoutes.when('/overview', { $scope.$watch( () => this.data, - data => { + async data => { + if (isEmpty(data)) { + return; + } + + let emailAddress = chrome.getInjected('monitoringLegacyEmailAddress') || ''; + if (KIBANA_ALERTING_ENABLED) { + emailAddress = config.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS) || emailAddress; + } + this.renderReact( <I18nContext> <SetupModeRenderer @@ -69,6 +85,7 @@ uiRoutes.when('/overview', { {flyoutComponent} <Overview cluster={data} + emailAddress={emailAddress} setupMode={setupMode} changeUrl={changeUrl} showLicenseExpiration={showLicenseExpiration} diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts new file mode 100644 index 0000000000000..2fec949f5692e --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts @@ -0,0 +1,453 @@ +/* + * 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 moment from 'moment-timezone'; +import { getLicenseExpiration } from './license_expiration'; +import { + ALERT_TYPE_LICENSE_EXPIRATION, + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, +} from '../../common/constants'; +import { Logger } from 'src/core/server'; +import { AlertServices } from '../../../alerting/server/types'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { AlertInstance } from '../../../alerting/server/alert_instance'; +import { + AlertState, + AlertClusterState, + AlertParams, + LicenseExpirationAlertExecutorOptions, +} from './types'; +import { SavedObject, SavedObjectAttributes } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; + +function fillLicense(license: any, clusterUuid?: string) { + return { + hits: { + hits: [ + { + _source: { + license, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + }; +} + +const clusterUuid = 'a4545jhjb'; +const params: AlertParams = { + dateFormat: 'YYYY', + timezone: 'UTC', +}; + +interface MockServices { + callCluster: jest.Mock; + alertInstanceFactory: jest.Mock; + savedObjectsClient: jest.Mock; +} + +const alertExecutorOptions: LicenseExpirationAlertExecutorOptions = { + alertId: '', + startedAt: new Date(), + services: { + callCluster: (path: string, opts: any) => new Promise(resolve => resolve()), + alertInstanceFactory: (id: string) => new AlertInstance(), + savedObjectsClient: {} as jest.Mocked<SavedObjectsClientContract>, + }, + params: {}, + state: {}, + spaceId: '', + name: '', + tags: [], + createdBy: null, + updatedBy: null, +}; + +describe('getLicenseExpiration', () => { + const emailAddress = 'foo@foo.com'; + const server: any = { + newPlatform: { + __internals: { + uiSettings: { + asScopedToClient: (): any => ({ + get: () => new Promise(resolve => resolve(emailAddress)), + }), + }, + }, + }, + }; + const getMonitoringCluster: () => void = jest.fn(); + const logger: Logger = { + warn: jest.fn(), + log: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + error: jest.fn(), + fatal: jest.fn(), + info: jest.fn(), + get: jest.fn(), + }; + const getLogger = (): Logger => logger; + const ccrEnabled = false; + + afterEach(() => { + (logger.warn as jest.Mock).mockClear(); + }); + + it('should have the right id and actionGroups', () => { + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + expect(alert.id).toBe(ALERT_TYPE_LICENSE_EXPIRATION); + expect(alert.actionGroups).toEqual(['default']); + }); + + it('should return the state if no license is provided', async () => { + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const services: MockServices | AlertServices = { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), + }; + const state = { foo: 1 }; + + const result = await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + }); + + expect(result).toEqual(state); + }); + + it('should log a warning if no email is provided', async () => { + const customServer: any = { + newPlatform: { + __internals: { + uiSettings: { + asScopedToClient: () => ({ + get: () => null, + }), + }, + }, + }, + }; + const alert = getLicenseExpiration(customServer, getMonitoringCluster, getLogger, ccrEnabled); + + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise<any> => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense({ + status: 'good', + type: 'basic', + expiry_date_in_millis: moment() + .add(7, 'days') + .valueOf(), + }) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), + }; + + const state = {}; + + await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + }); + + expect((logger.warn as jest.Mock).mock.calls.length).toBe(1); + expect(logger.warn).toHaveBeenCalledWith( + `Unable to send email for ${ALERT_TYPE_LICENSE_EXPIRATION} because there is no email configured.` + ); + }); + + it('should fire actions if going to expire', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject<SavedObjectAttributes> = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise<any> => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'gold', + expiry_date_in_millis: moment() + .add(7, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state = {}; + + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + + expect(newState.expiredCheckDateMS > 0).toBe(true); + expect(scheduleActions.mock.calls.length).toBe(1); + expect(scheduleActions.mock.calls[0][1].subject).toBe( + 'NEW X-Pack Monitoring: License Expiration' + ); + expect(scheduleActions.mock.calls[0][1].to).toBe(emailAddress); + }); + + it('should fire actions if the user fixed their license', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject<SavedObjectAttributes> = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise<any> => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'gold', + expiry_date_in_millis: moment() + .add(120, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state: AlertState = { + [clusterUuid]: { + expiredCheckDateMS: moment() + .subtract(1, 'day') + .valueOf(), + ui: { isFiring: true, severity: 0, message: null, resolvedMS: 0, expirationTime: 0 }, + }, + }; + + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + expect(newState.expiredCheckDateMS).toBe(0); + expect(scheduleActions.mock.calls.length).toBe(1); + expect(scheduleActions.mock.calls[0][1].subject).toBe( + 'RESOLVED X-Pack Monitoring: License Expiration' + ); + expect(scheduleActions.mock.calls[0][1].to).toBe(emailAddress); + }); + + it('should not fire actions for trial license that expire in more than 14 days', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject<SavedObjectAttributes> = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise<any> => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'trial', + expiry_date_in_millis: moment() + .add(15, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state = {}; + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + expect(newState.expiredCheckDateMS).toBe(undefined); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should fire actions for trial license that in 14 days or less', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject<SavedObjectAttributes> = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise<any> => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'trial', + expiry_date_in_millis: moment() + .add(13, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state = {}; + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + expect(newState.expiredCheckDateMS > 0).toBe(true); + expect(scheduleActions.mock.calls.length).toBe(1); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts new file mode 100644 index 0000000000000..197c5c9cdcbc7 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment-timezone'; +import { get } from 'lodash'; +import { Legacy } from 'kibana'; +import { Logger } from 'src/core/server'; +import { ALERT_TYPE_LICENSE_EXPIRATION, INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; +import { AlertType } from '../../../alerting'; +import { fetchLicenses } from '../lib/alerts/fetch_licenses'; +import { fetchDefaultEmailAddress } from '../lib/alerts/fetch_default_email_address'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; +import { + AlertLicense, + AlertState, + AlertClusterState, + AlertClusterUiState, + LicenseExpirationAlertExecutorOptions, +} from './types'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { executeActions, getUiMessage } from '../lib/alerts/license_expiration.lib'; + +const EXPIRES_DAYS = [60, 30, 14, 7]; + +export const getLicenseExpiration = ( + server: Legacy.Server, + getMonitoringCluster: any, + getLogger: (contexts: string[]) => Logger, + ccsEnabled: boolean +): AlertType => { + async function getCallCluster(services: any): Promise<any> { + const monitoringCluster = await getMonitoringCluster(); + if (!monitoringCluster) { + return services.callCluster; + } + + return monitoringCluster.callCluster; + } + + const logger = getLogger([ALERT_TYPE_LICENSE_EXPIRATION]); + return { + id: ALERT_TYPE_LICENSE_EXPIRATION, + name: 'Monitoring Alert - License Expiration', + actionGroups: ['default'], + async executor({ + services, + params, + state, + }: LicenseExpirationAlertExecutorOptions): Promise<any> { + logger.debug( + `Firing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` + ); + + const callCluster = await getCallCluster(services); + + // Support CCS use cases by querying to find available remote clusters + // and then adding those to the index pattern we are searching against + let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; + if (ccsEnabled) { + const availableCcs = await fetchAvailableCcs(callCluster); + if (availableCcs.length > 0) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + } + + const clusters = await fetchClusters(callCluster, esIndexPattern); + + // Fetch licensing information from cluster_stats documents + const licenses: AlertLicense[] = await fetchLicenses(callCluster, clusters, esIndexPattern); + if (licenses.length === 0) { + logger.warn(`No license found for ${ALERT_TYPE_LICENSE_EXPIRATION}.`); + return state; + } + + const uiSettings = server.newPlatform.__internals.uiSettings.asScopedToClient( + services.savedObjectsClient + ); + const dateFormat: string = await uiSettings.get<string>('dateFormat'); + const timezone: string = await uiSettings.get<string>('dateFormat:tz'); + const emailAddress = await fetchDefaultEmailAddress(uiSettings); + if (!emailAddress) { + // TODO: we can do more here + logger.warn( + `Unable to send email for ${ALERT_TYPE_LICENSE_EXPIRATION} because there is no email configured.` + ); + return; + } + + const result: AlertState = { ...state }; + + for (const license of licenses) { + const licenseState: AlertClusterState = state[license.clusterUuid] || {}; + const $expiry = moment.utc(license.expiryDateMS); + let isExpired = false; + let severity = 0; + + if (license.status !== 'active') { + isExpired = true; + severity = 2001; + } else if (license.expiryDateMS) { + for (let i = EXPIRES_DAYS.length - 1; i >= 0; i--) { + if (license.type === 'trial' && i < 2) { + break; + } + + const $fromNow = moment.utc().add(EXPIRES_DAYS[i], 'days'); + if ($fromNow.isAfter($expiry)) { + isExpired = true; + severity = 1000 * i; + break; + } + } + } + + const ui: AlertClusterUiState = get<AlertClusterUiState>(licenseState, 'ui', { + isFiring: false, + message: null, + severity: 0, + resolvedMS: 0, + expirationTime: 0, + }); + let resolved = ui.resolvedMS; + let message = ui.message; + let expiredCheckDate = licenseState.expiredCheckDateMS; + const instance = services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION); + + if (isExpired) { + if (!licenseState.expiredCheckDateMS) { + logger.debug(`License will expire soon, sending email`); + executeActions(instance, license, $expiry, dateFormat, emailAddress); + expiredCheckDate = moment().valueOf(); + } + message = getUiMessage(license, timezone); + resolved = 0; + } else if (!isExpired && licenseState.expiredCheckDateMS) { + logger.debug(`License expiration has been resolved, sending email`); + executeActions(instance, license, $expiry, dateFormat, emailAddress, true); + expiredCheckDate = 0; + message = getUiMessage(license, timezone, true); + resolved = moment().valueOf(); + } + + result[license.clusterUuid] = { + expiredCheckDateMS: expiredCheckDate, + ui: { + message, + expirationTime: license.expiryDateMS, + isFiring: expiredCheckDate > 0, + severity, + resolvedMS: resolved, + }, + }; + } + + return result; + }, + }; +}; diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/types.d.ts b/x-pack/legacy/plugins/monitoring/server/alerts/types.d.ts new file mode 100644 index 0000000000000..6346ca00dabbd --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/alerts/types.d.ts @@ -0,0 +1,45 @@ +/* + * 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 { Moment } from 'moment'; +import { AlertExecutorOptions } from '../../../alerting'; + +export interface AlertLicense { + status: string; + type: string; + expiryDateMS: number; + clusterUuid: string; + clusterName: string; +} + +export interface AlertState { + [clusterUuid: string]: AlertClusterState; +} + +export interface AlertClusterState { + expiredCheckDateMS: number | Moment; + ui: AlertClusterUiState; +} + +export interface AlertClusterUiState { + isFiring: boolean; + severity: number; + message: string | null; + resolvedMS: number; + expirationTime: number; +} + +export interface AlertCluster { + clusterUuid: string; +} + +export interface LicenseExpirationAlertExecutorOptions extends AlertExecutorOptions { + state: AlertState; +} + +export interface AlertParams { + dateFormat: string; + timezone: string; +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts new file mode 100644 index 0000000000000..4398b2dd675ec --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts @@ -0,0 +1,36 @@ +/* + * 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 { fetchAvailableCcs } from './fetch_available_ccs'; + +describe('fetchAvailableCcs', () => { + it('should call the `cluster.remoteInfo` api', async () => { + const callCluster = jest.fn(); + await fetchAvailableCcs(callCluster); + expect(callCluster).toHaveBeenCalledWith('cluster.remoteInfo'); + }); + + it('should return clusters that are connected', async () => { + const connectedRemote = 'myRemote'; + const callCluster = jest.fn().mockImplementation(() => ({ + [connectedRemote]: { + connected: true, + }, + })); + const result = await fetchAvailableCcs(callCluster); + expect(result).toEqual([connectedRemote]); + }); + + it('should not return clusters that are connected', async () => { + const disconnectedRemote = 'myRemote'; + const callCluster = jest.fn().mockImplementation(() => ({ + [disconnectedRemote]: { + connected: false, + }, + })); + const result = await fetchAvailableCcs(callCluster); + expect(result.length).toBe(0); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts new file mode 100644 index 0000000000000..34efaff93f34c --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export async function fetchAvailableCcs(callCluster: any): Promise<string[]> { + const availableCcs = []; + const response = await callCluster('cluster.remoteInfo'); + for (const remoteName in response) { + if (!response.hasOwnProperty(remoteName)) { + continue; + } + const remoteInfo = response[remoteName]; + if (remoteInfo.connected) { + availableCcs.push(remoteName); + } + } + return availableCcs; +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts new file mode 100644 index 0000000000000..78eb9773df15f --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { fetchClusters } from './fetch_clusters'; + +describe('fetchClusters', () => { + it('return a list of clusters', async () => { + const callCluster = jest.fn().mockImplementation(() => ({ + aggregations: { + clusters: { + buckets: [ + { + key: 'clusterA', + }, + ], + }, + }, + })); + const index = '.monitoring-es-*'; + const result = await fetchClusters(callCluster, index); + expect(result).toEqual([{ clusterUuid: 'clusterA' }]); + }); + + it('should limit the time period in the query', async () => { + const callCluster = jest.fn(); + const index = '.monitoring-es-*'; + await fetchClusters(callCluster, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[1].range.timestamp.gte).toBe('now-2m'); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.ts new file mode 100644 index 0000000000000..8ef7339618a2c --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.ts @@ -0,0 +1,52 @@ +/* + * 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 { get } from 'lodash'; +import { AlertCluster } from '../../alerts/types'; + +interface AggregationResult { + key: string; +} + +export async function fetchClusters(callCluster: any, index: string): Promise<AlertCluster[]> { + const params = { + index, + filterPath: 'aggregations.clusters.buckets', + body: { + size: 0, + query: { + bool: { + filter: [ + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: { + field: 'cluster_uuid', + size: 1000, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + return get(response, 'aggregations.clusters.buckets', []).map((bucket: AggregationResult) => ({ + clusterUuid: bucket.key, + })); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts new file mode 100644 index 0000000000000..25b09b956038a --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts @@ -0,0 +1,17 @@ +/* + * 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 { fetchDefaultEmailAddress } from './fetch_default_email_address'; +import { uiSettingsServiceMock } from '../../../../../../../src/core/server/mocks'; + +describe('fetchDefaultEmailAddress', () => { + it('get the email address', async () => { + const email = 'test@test.com'; + const uiSettingsClient = uiSettingsServiceMock.createClient(); + uiSettingsClient.get.mockResolvedValue(email); + const result = await fetchDefaultEmailAddress(uiSettingsClient); + expect(result).toBe(email); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts new file mode 100644 index 0000000000000..88e4199a88256 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient } from 'src/core/server'; +import { MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS } from '../../../common/constants'; + +export async function fetchDefaultEmailAddress( + uiSettingsClient: IUiSettingsClient +): Promise<string> { + return await uiSettingsClient.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts new file mode 100644 index 0000000000000..dd6c074e68b1f --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts @@ -0,0 +1,105 @@ +/* + * 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 { fetchLicenses } from './fetch_licenses'; + +describe('fetchLicenses', () => { + it('return a list of licenses', async () => { + const clusterName = 'MyCluster'; + const clusterUuid = 'clusterA'; + const license = { + status: 'active', + expiry_date_in_millis: 1579532493876, + type: 'basic', + }; + const callCluster = jest.fn().mockImplementation(() => ({ + hits: { + hits: [ + { + _source: { + license, + cluster_name: clusterName, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + })); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + const result = await fetchLicenses(callCluster, clusters, index); + expect(result).toEqual([ + { + status: license.status, + type: license.type, + expiryDateMS: license.expiry_date_in_millis, + clusterUuid, + clusterName, + }, + ]); + }); + + it('should only search for the clusters provided', async () => { + const clusterUuid = 'clusterA'; + const callCluster = jest.fn(); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[0].terms.cluster_uuid).toEqual([clusterUuid]); + }); + + it('should limit the time period in the query', async () => { + const clusterUuid = 'clusterA'; + const callCluster = jest.fn(); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[2].range.timestamp.gte).toBe('now-2m'); + }); + + it('should give priority to the metadata name', async () => { + const clusterName = 'MyCluster'; + const clusterUuid = 'clusterA'; + const license = { + status: 'active', + expiry_date_in_millis: 1579532493876, + type: 'basic', + }; + const callCluster = jest.fn().mockImplementation(() => ({ + hits: { + hits: [ + { + _source: { + license, + cluster_name: 'fakeName', + cluster_uuid: clusterUuid, + cluster_settings: { + cluster: { + metadata: { + display_name: clusterName, + }, + }, + }, + }, + }, + ], + }, + })); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + const result = await fetchLicenses(callCluster, clusters, index); + expect(result).toEqual([ + { + status: license.status, + type: license.type, + expiryDateMS: license.expiry_date_in_millis, + clusterUuid, + clusterName, + }, + ]); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.ts new file mode 100644 index 0000000000000..31a68e8aa9c3e --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -0,0 +1,67 @@ +/* + * 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 { get } from 'lodash'; +import { AlertLicense, AlertCluster } from '../../alerts/types'; + +export async function fetchLicenses( + callCluster: any, + clusters: AlertCluster[], + index: string +): Promise<AlertLicense[]> { + const params = { + index, + filterPath: [ + 'hits.hits._source.license.*', + 'hits.hits._source.cluster_settings.cluster.metadata.display_name', + 'hits.hits._source.cluster_uuid', + 'hits.hits._source.cluster_name', + ], + body: { + size: 1, + sort: [{ timestamp: { order: 'desc' } }], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map(cluster => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + }, + }; + + const response = await callCluster('search', params); + return get<any>(response, 'hits.hits', []).map((hit: any) => { + const clusterName: string = + get(hit, '_source.cluster_settings.cluster.metadata.display_name') || + get(hit, '_source.cluster_name') || + get(hit, '_source.cluster_uuid'); + const rawLicense: any = get(hit, '_source.license', {}); + const license: AlertLicense = { + status: rawLicense.status, + type: rawLicense.type, + expiryDateMS: rawLicense.expiry_date_in_millis, + clusterUuid: get(hit, '_source.cluster_uuid'), + clusterName, + }; + return license; + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_status.ts new file mode 100644 index 0000000000000..9f7c1d5a994d2 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -0,0 +1,87 @@ +/* + * 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 moment from 'moment'; +import { get } from 'lodash'; +import { AlertClusterState } from '../../alerts/types'; +import { ALERT_TYPES, LOGGING_TAG } from '../../../common/constants'; + +export async function fetchStatus( + callCluster: any, + start: number, + end: number, + clusterUuid: string, + server: any +): Promise<any[]> { + // TODO: this shouldn't query task manager directly but rather + // use an api exposed by the alerting/actions plugin + // See https://github.com/elastic/kibana/issues/48442 + const statuses = await Promise.all( + ALERT_TYPES.map( + type => + new Promise(async (resolve, reject) => { + try { + const params = { + index: '.kibana_task_manager', + filterPath: ['hits.hits._source.task.state'], + body: { + size: 1, + sort: [{ updated_at: { order: 'desc' } }], + query: { + bool: { + filter: [ + { + term: { + 'task.taskType': `alerting:${type}`, + }, + }, + ], + }, + }, + }, + }; + + const response = await callCluster('search', params); + const state = get(response, 'hits.hits[0]._source.task.state', '{}'); + const clusterState: AlertClusterState = get<AlertClusterState>( + JSON.parse(state), + `alertTypeState.${clusterUuid}`, + { + expiredCheckDateMS: 0, + ui: { + isFiring: false, + message: null, + severity: 0, + resolvedMS: 0, + expirationTime: 0, + }, + } + ); + const isInBetween = moment(clusterState.ui.resolvedMS).isBetween(start, end); + if (clusterState.ui.isFiring || isInBetween) { + return resolve({ + type, + ...clusterState.ui, + }); + } + return resolve(false); + } catch (err) { + const reason = get(err, 'body.error.type'); + if (reason === 'index_not_found_exception') { + server.log( + ['error', LOGGING_TAG], + `Unable to fetch alerts. Alerts depends on task manager, which has not been started yet.` + ); + } else { + server.log(['error', LOGGING_TAG], err.message); + } + return resolve(false); + } + }) + ) + ); + + return statuses.filter(Boolean); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.test.ts new file mode 100644 index 0000000000000..a5eb104986161 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.test.ts @@ -0,0 +1,24 @@ +/* + * 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 { getCcsIndexPattern } from './get_ccs_index_pattern'; + +describe('getCcsIndexPattern', () => { + it('should return an index pattern including remotes', () => { + const remotes = ['Remote1', 'Remote2']; + const index = '.monitoring-es-*'; + const result = getCcsIndexPattern(index, remotes); + expect(result).toBe('.monitoring-es-*,Remote1:.monitoring-es-*,Remote2:.monitoring-es-*'); + }); + + it('should return an index pattern from multiple index patterns including remotes', () => { + const remotes = ['Remote1', 'Remote2']; + const index = '.monitoring-es-*,.monitoring-kibana-*'; + const result = getCcsIndexPattern(index, remotes); + expect(result).toBe( + '.monitoring-es-*,.monitoring-kibana-*,Remote1:.monitoring-es-*,Remote2:.monitoring-es-*,Remote1:.monitoring-kibana-*,Remote2:.monitoring-kibana-*' + ); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts new file mode 100644 index 0000000000000..b562fde2a0810 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export function getCcsIndexPattern(indexPattern: string, remotes: string[]): string { + return `${indexPattern},${indexPattern + .split(',') + .map(pattern => { + return remotes.map(remoteName => `${remoteName}:${pattern}`).join(','); + }) + .join(',')}`; +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts new file mode 100644 index 0000000000000..1a2eb1e44be84 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts @@ -0,0 +1,55 @@ +/* + * 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 moment from 'moment-timezone'; +import { executeActions, getUiMessage } from './license_expiration.lib'; + +describe('licenseExpiration lib', () => { + describe('executeActions', () => { + const clusterName = 'clusterA'; + const instance: any = { scheduleActions: jest.fn() }; + const license: any = { clusterName }; + const $expiry = moment('2020-01-20'); + const dateFormat = 'dddd, MMMM Do YYYY, h:mm:ss a'; + const emailAddress = 'test@test.com'; + + beforeEach(() => { + instance.scheduleActions.mockClear(); + }); + + it('should schedule actions when firing', () => { + executeActions(instance, license, $expiry, dateFormat, emailAddress, false); + expect(instance.scheduleActions).toHaveBeenCalledWith('default', { + subject: 'NEW X-Pack Monitoring: License Expiration', + message: `Cluster '${clusterName}' license is going to expire on Monday, January 20th 2020, 12:00:00 am. Please update your license.`, + to: emailAddress, + }); + }); + + it('should schedule actions when resolved', () => { + executeActions(instance, license, $expiry, dateFormat, emailAddress, true); + expect(instance.scheduleActions).toHaveBeenCalledWith('default', { + subject: 'RESOLVED X-Pack Monitoring: License Expiration', + message: `This cluster alert has been resolved: Cluster '${clusterName}' license was going to expire on Monday, January 20th 2020, 12:00:00 am.`, + to: emailAddress, + }); + }); + }); + + describe('getUiMessage', () => { + const timezone = 'Europe/London'; + const license: any = { expiryDateMS: moment.tz('2020-01-20 08:00:00', timezone).utc() }; + + it('should return a message when firing', () => { + const message = getUiMessage(license, timezone, false); + expect(message).toBe(`This cluster's license is going to expire in #relative at #absolute.`); + }); + + it('should return a message when resolved', () => { + const message = getUiMessage(license, timezone, true); + expect(message).toBe(`This cluster's license is active.`); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts new file mode 100644 index 0000000000000..8a75fc1fbbd82 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts @@ -0,0 +1,58 @@ +/* + * 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 { Moment } from 'moment-timezone'; +import { i18n } from '@kbn/i18n'; +import { AlertInstance } from '../../../../alerting/server/alert_instance'; +import { AlertLicense } from '../../alerts/types'; + +const RESOLVED_SUBJECT = i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.resolvedSubject', + { + defaultMessage: 'RESOLVED X-Pack Monitoring: License Expiration', + } +); + +const NEW_SUBJECT = i18n.translate('xpack.monitoring.alerts.licenseExpiration.newSubject', { + defaultMessage: 'NEW X-Pack Monitoring: License Expiration', +}); + +export function executeActions( + instance: AlertInstance, + license: AlertLicense, + $expiry: Moment, + dateFormat: string, + emailAddress: string, + resolved: boolean = false +) { + if (resolved) { + instance.scheduleActions('default', { + subject: RESOLVED_SUBJECT, + message: `This cluster alert has been resolved: Cluster '${ + license.clusterName + }' license was going to expire on ${$expiry.format(dateFormat)}.`, + to: emailAddress, + }); + } else { + instance.scheduleActions('default', { + subject: NEW_SUBJECT, + message: `Cluster '${license.clusterName}' license is going to expire on ${$expiry.format( + dateFormat + )}. Please update your license.`, + to: emailAddress, + }); + } +} + +export function getUiMessage(license: AlertLicense, timezone: string, resolved: boolean = false) { + if (resolved) { + return i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage', { + defaultMessage: `This cluster's license is active.`, + }); + } + return i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', { + defaultMessage: `This cluster's license is going to expire in #relative at #absolute.`, + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index 2b080a5c333fc..a5426dc04545e 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -16,6 +16,7 @@ import { getBeatsForClusters } from '../beats'; import { alertsClustersAggregation } from '../../cluster_alerts/alerts_clusters_aggregation'; import { alertsClusterSearch } from '../../cluster_alerts/alerts_cluster_search'; import { checkLicense as checkLicenseForAlerts } from '../../cluster_alerts/check_license'; +import { fetchStatus } from '../alerts/fetch_status'; import { getClustersSummary } from './get_clusters_summary'; import { CLUSTER_ALERTS_SEARCH_SIZE, @@ -27,6 +28,7 @@ import { CODE_PATH_LOGSTASH, CODE_PATH_BEATS, CODE_PATH_APM, + KIBANA_ALERTING_ENABLED, } from '../../../common/constants'; import { getApmsForClusters } from '../apm/get_apms_for_clusters'; import { i18n } from '@kbn/i18n'; @@ -99,15 +101,31 @@ export async function getClustersFromRequest( if (mlJobs !== null) { cluster.ml = { jobs: mlJobs }; } - const alerts = isInCodePath(codePaths, [CODE_PATH_ALERTS]) - ? await alertsClusterSearch(req, alertsIndex, cluster, checkLicenseForAlerts, { + + if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) { + if (KIBANA_ALERTING_ENABLED) { + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); + const callCluster = (...args) => callWithRequest(req, ...args); + cluster.alerts = await fetchStatus( + callCluster, start, end, - size: CLUSTER_ALERTS_SEARCH_SIZE, - }) - : null; - if (alerts) { - cluster.alerts = alerts; + cluster.cluster_uuid, + req.server + ); + } else { + cluster.alerts = await alertsClusterSearch( + req, + alertsIndex, + cluster, + checkLicenseForAlerts, + { + start, + end, + size: CLUSTER_ALERTS_SEARCH_SIZE, + } + ); + } } cluster.logs = isInCodePath(codePaths, [CODE_PATH_LOGS]) diff --git a/x-pack/legacy/plugins/monitoring/server/lib/get_date_format.js b/x-pack/legacy/plugins/monitoring/server/lib/get_date_format.js new file mode 100644 index 0000000000000..89cbf20d9b56f --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/get_date_format.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export async function getDateFormat(req) { + return await req.getUiSettingsService().get('dateFormat'); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index 5f52e0c6a983b..a12b48510a6ff 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -348,7 +348,6 @@ export const getCollectionStatus = async ( }, }; } - const liveClusterUuid = skipLiveData ? null : await getLiveElasticsearchClusterUuid(req); const isLiveCluster = !clusterUuid || liveClusterUuid === clusterUuid; diff --git a/x-pack/legacy/plugins/monitoring/server/plugin.js b/x-pack/legacy/plugins/monitoring/server/plugin.js index ef346e95ad075..50e5319a0f526 100644 --- a/x-pack/legacy/plugins/monitoring/server/plugin.js +++ b/x-pack/legacy/plugins/monitoring/server/plugin.js @@ -5,12 +5,17 @@ */ import { i18n } from '@kbn/i18n'; -import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG } from '../common/constants'; +import { + LOGGING_TAG, + KIBANA_MONITORING_LOGGING_TAG, + KIBANA_ALERTING_ENABLED, +} from '../common/constants'; import { requireUIRoutes } from './routes'; import { instantiateClient } from './es_client/instantiate_client'; import { initMonitoringXpackInfo } from './init_monitoring_xpack_info'; import { initBulkUploader, registerCollectors } from './kibana_monitoring'; import { registerMonitoringCollection } from './telemetry_collection'; +import { getLicenseExpiration } from './alerts/license_expiration'; import { parseElasticsearchConfig } from './es_client/parse_elasticsearch_config'; export class Plugin { @@ -133,5 +138,37 @@ export class Plugin { showCgroupMetricsLogstash: config.get('monitoring.ui.container.logstash.enabled'), // Note, not currently used, but see https://github.com/elastic/x-pack-kibana/issues/1559 part 2 }; }); + + if (KIBANA_ALERTING_ENABLED && plugins.alerting) { + // this is not ready right away but we need to register alerts right away + async function getMonitoringCluster() { + const configs = config.get('xpack.monitoring.elasticsearch'); + if (configs.hosts) { + const monitoringCluster = plugins.elasticsearch.getCluster('monitoring'); + const { username, password } = configs; + const fakeRequest = { + headers: { + authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`, + }, + }; + return { + callCluster: (...args) => monitoringCluster.callWithRequest(fakeRequest, ...args), + }; + } + return null; + } + + function getLogger(contexts) { + return core.logger.get('plugins', LOGGING_TAG, ...contexts); + } + plugins.alerting.setup.registerType( + getLicenseExpiration( + core._hapi, + getMonitoringCluster, + getLogger, + config.get('xpack.monitoring.ccs.enabled') + ) + ); + } } } diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/alerts.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/alerts.js new file mode 100644 index 0000000000000..f87683effe437 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/alerts.js @@ -0,0 +1,89 @@ +/* + * 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 Joi from 'joi'; +import { isFunction } from 'lodash'; +import { + ALERT_TYPE_LICENSE_EXPIRATION, + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, +} from '../../../../../common/constants'; + +async function createAlerts(req, alertsClient, { selectedEmailActionId }) { + const createdAlerts = []; + + // Create alerts + const ALERT_TYPES = { + [ALERT_TYPE_LICENSE_EXPIRATION]: { + schedule: { interval: '10s' }, + actions: [ + { + group: 'default', + id: selectedEmailActionId, + params: { + subject: '{{context.subject}}', + message: `{{context.message}}`, + to: ['{{context.to}}'], + }, + }, + ], + }, + }; + + for (const alertTypeId of Object.keys(ALERT_TYPES)) { + const existingAlert = await alertsClient.find({ + options: { + search: alertTypeId, + }, + }); + if (existingAlert.total === 1) { + await alertsClient.delete({ id: existingAlert.data[0].id }); + } + + const result = await alertsClient.create({ + data: { + enabled: true, + alertTypeId, + ...ALERT_TYPES[alertTypeId], + }, + }); + createdAlerts.push(result); + } + + return createdAlerts; +} + +async function saveEmailAddress(emailAddress, uiSettingsService) { + await uiSettingsService.set(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, emailAddress); +} + +export function createKibanaAlertsRoute(server) { + server.route({ + method: 'POST', + path: '/api/monitoring/v1/alerts', + config: { + validate: { + payload: Joi.object({ + selectedEmailActionId: Joi.string().required(), + emailAddress: Joi.string().required(), + }), + }, + }, + async handler(req, headers) { + const { emailAddress, selectedEmailActionId } = req.payload; + const alertsClient = isFunction(req.getAlertsClient) ? req.getAlertsClient() : null; + if (!alertsClient) { + return headers.response().code(404); + } + + const [alerts, emailResponse] = await Promise.all([ + createAlerts(req, alertsClient, { ...req.params, selectedEmailActionId }), + saveEmailAddress(emailAddress, req.getUiSettingsService()), + ]); + + return { alerts, emailResponse }; + }, + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js index cdcd776b349fc..246cdfde97cff 100644 --- a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js @@ -4,54 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; -import { alertsClusterSearch } from '../../../../cluster_alerts/alerts_cluster_search'; -import { checkLicense } from '../../../../cluster_alerts/check_license'; -import { getClusterLicense } from '../../../../lib/cluster/get_cluster_license'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; -import { INDEX_PATTERN_ELASTICSEARCH, INDEX_ALERTS } from '../../../../../common/constants'; - -/* - * Cluster Alerts route. - */ -export function clusterAlertsRoute(server) { - server.route({ - method: 'POST', - path: '/api/monitoring/v1/clusters/{clusterUuid}/alerts', - config: { - validate: { - params: Joi.object({ - clusterUuid: Joi.string().required(), - }), - payload: Joi.object({ - ccs: Joi.string().optional(), - timeRange: Joi.object({ - min: Joi.date().required(), - max: Joi.date().required(), - }).required(), - }), - }, - }, - handler(req) { - const config = server.config(); - const ccs = req.payload.ccs; - const clusterUuid = req.params.clusterUuid; - const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); - const alertsIndex = prefixIndexPattern(config, INDEX_ALERTS, ccs); - const options = { - start: req.payload.timeRange.min, - end: req.payload.timeRange.max, - }; - - return getClusterLicense(req, esIndexPattern, clusterUuid).then(license => - alertsClusterSearch( - req, - alertsIndex, - { cluster_uuid: clusterUuid, license }, - checkLicense, - options - ) - ); - }, - }); -} +export * from './legacy_alerts'; +export * from './alerts'; diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js new file mode 100644 index 0000000000000..a3049f0f3e2d2 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js @@ -0,0 +1,57 @@ +/* + * 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 Joi from 'joi'; +import { alertsClusterSearch } from '../../../../cluster_alerts/alerts_cluster_search'; +import { checkLicense } from '../../../../cluster_alerts/check_license'; +import { getClusterLicense } from '../../../../lib/cluster/get_cluster_license'; +import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { INDEX_PATTERN_ELASTICSEARCH, INDEX_ALERTS } from '../../../../../common/constants'; + +/* + * Cluster Alerts route. + */ +export function legacyClusterAlertsRoute(server) { + server.route({ + method: 'POST', + path: '/api/monitoring/v1/clusters/{clusterUuid}/legacy_alerts', + config: { + validate: { + params: Joi.object({ + clusterUuid: Joi.string().required(), + }), + payload: Joi.object({ + ccs: Joi.string().optional(), + timeRange: Joi.object({ + min: Joi.date().required(), + max: Joi.date().required(), + }).required(), + }), + }, + }, + handler(req) { + const config = server.config(); + const ccs = req.payload.ccs; + const clusterUuid = req.params.clusterUuid; + const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); + const alertsIndex = prefixIndexPattern(config, INDEX_ALERTS, ccs); + const options = { + start: req.payload.timeRange.min, + end: req.payload.timeRange.max, + }; + + return getClusterLicense(req, esIndexPattern, clusterUuid).then(license => + alertsClusterSearch( + req, + alertsIndex, + { cluster_uuid: clusterUuid, license }, + checkLicense, + options + ) + ); + }, + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js index baffbfd5f3f6f..de0213ec84689 100644 --- a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js @@ -6,7 +6,7 @@ // all routes for the app export { checkAccessRoute } from './check_access'; -export { clusterAlertsRoute } from './alerts/'; +export * from './alerts/'; export { beatsDetailRoute, beatsListingRoute, beatsOverviewRoute } from './beats'; export { clusterRoute, clustersRoute } from './cluster'; export { diff --git a/x-pack/legacy/plugins/monitoring/ui_exports.js b/x-pack/legacy/plugins/monitoring/ui_exports.js index 9251deb673bd1..49f167b0f1b10 100644 --- a/x-pack/legacy/plugins/monitoring/ui_exports.js +++ b/x-pack/legacy/plugins/monitoring/ui_exports.js @@ -6,6 +6,10 @@ import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; +import { + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, + KIBANA_ALERTING_ENABLED, +} from './common/constants'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; /** @@ -14,28 +18,48 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; * app (injectDefaultVars and hacks) * @return {Object} data per Kibana plugin uiExport schema */ -export const getUiExports = () => ({ - app: { - title: i18n.translate('xpack.monitoring.stackMonitoringTitle', { - defaultMessage: 'Stack Monitoring', - }), - order: 9002, - description: i18n.translate('xpack.monitoring.uiExportsDescription', { - defaultMessage: 'Monitoring for Elastic Stack', - }), - icon: 'plugins/monitoring/icons/monitoring.svg', - euiIconType: 'monitoringApp', - linkToLastSubUrl: false, - main: 'plugins/monitoring/monitoring', - category: DEFAULT_APP_CATEGORIES.management, - }, - injectDefaultVars(server) { - const config = server.config(); - return { - monitoringUiEnabled: config.get('monitoring.ui.enabled'), +export const getUiExports = () => { + const uiSettingDefaults = {}; + if (KIBANA_ALERTING_ENABLED) { + uiSettingDefaults[MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS] = { + name: i18n.translate('xpack.monitoring.alertingEmailAddress.name', { + defaultMessage: 'Alerting email address', + }), + value: '', + description: i18n.translate('xpack.monitoring.alertingEmailAddress.description', { + defaultMessage: `The default email address to receive alerts from Stack Monitoring`, + }), + category: ['monitoring'], }; - }, - hacks: ['plugins/monitoring/hacks/toggle_app_link_in_nav'], - home: ['plugins/monitoring/register_feature'], - styleSheetPaths: resolve(__dirname, 'public/index.scss'), -}); + } + + return { + app: { + title: i18n.translate('xpack.monitoring.stackMonitoringTitle', { + defaultMessage: 'Stack Monitoring', + }), + order: 9002, + description: i18n.translate('xpack.monitoring.uiExportsDescription', { + defaultMessage: 'Monitoring for Elastic Stack', + }), + icon: 'plugins/monitoring/icons/monitoring.svg', + euiIconType: 'monitoringApp', + linkToLastSubUrl: false, + main: 'plugins/monitoring/monitoring', + category: DEFAULT_APP_CATEGORIES.management, + }, + injectDefaultVars(server) { + const config = server.config(); + return { + monitoringUiEnabled: config.get('monitoring.ui.enabled'), + monitoringLegacyEmailAddress: config.get( + 'monitoring.cluster_alerts.email_notifications.email_address' + ), + }; + }, + uiSettingDefaults, + hacks: ['plugins/monitoring/hacks/toggle_app_link_in_nav'], + home: ['plugins/monitoring/register_feature'], + styleSheetPaths: resolve(__dirname, 'public/index.scss'), + }; +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js index 06ba36f600b9b..b21d628332027 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js @@ -11,6 +11,7 @@ import { CancellationToken } from '../../../common/cancellation_token'; import { fieldFormats } from '../../../../../../../src/plugins/data/server'; import { LevelLogger } from '../../../server/lib/level_logger'; import { executeJobFactory } from './execute_job'; +import { setFieldFormats } from '../../../server/services'; const delay = ms => new Promise(resolve => setTimeout(() => resolve(), ms)); @@ -73,7 +74,7 @@ describe('CSV Execute Job', function() { uiSettingsGetStub.withArgs('csv:separator').returns(','); uiSettingsGetStub.withArgs('csv:quoteValues').returns(true); - mockServer = { + setFieldFormats({ fieldFormatServiceFactory: function() { const uiConfigMock = {}; uiConfigMock['format:defaultTypeMap'] = { @@ -86,6 +87,17 @@ describe('CSV Execute Job', function() { return fieldFormatsRegistry; }, + }); + + mockServer = { + expose: function() {}, + plugins: { + elasticsearch: { + getCluster: function() { + return clusterStub; + }, + }, + }, config: function() { return { get: configGetStub, diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts index 280bbf13fa992..9f94a755cf655 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts @@ -13,6 +13,7 @@ import { ESQueueWorkerExecuteFn, ExecuteJobFactory, Logger, ServerFacade } from import { JobDocPayloadDiscoverCsv } from '../types'; import { fieldFormatMapFactory } from './lib/field_format_map'; import { createGenerateCsv } from './lib/generate_csv'; +import { getFieldFormats } from '../../../server/services'; export const executeJobFactory: ExecuteJobFactory<ESQueueWorkerExecuteFn< JobDocPayloadDiscoverCsv @@ -94,7 +95,7 @@ export const executeJobFactory: ExecuteJobFactory<ESQueueWorkerExecuteFn< const [formatsMap, uiSettings] = await Promise.all([ (async () => { - const fieldFormats = await server.fieldFormatServiceFactory(uiConfig); + const fieldFormats = await getFieldFormats().fieldFormatServiceFactory(uiConfig); return fieldFormatMapFactory(indexPatternSavedObject, fieldFormats); })(), (async () => { diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index 966e4ff209ad6..cbafc4b1ecc4b 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -6,23 +6,16 @@ import { i18n } from '@kbn/i18n'; import { Legacy } from 'kibana'; -import { IUiSettingsClient } from 'kibana/server'; import { resolve } from 'path'; -import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; -import { SecurityPluginSetup } from '../../../plugins/security/server'; import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants'; import { config as reportingConfig } from './config'; -import { LegacySetup, ReportingPlugin, reportingPluginFactory } from './server/plugin'; +import { legacyInit } from './server/legacy'; import { ReportingConfigOptions, ReportingPluginSpecOptions } from './types.d'; const kbToBase64Length = (kb: number) => { return Math.floor((kb * 1024 * 8) / 6); }; -interface ReportingDeps { - data: DataPluginStart; -} - export const reporting = (kibana: any) => { return new kibana.Plugin({ id: PLUGIN_ID, @@ -68,35 +61,7 @@ export const reporting = (kibana: any) => { }, async init(server: Legacy.Server) { - const coreSetup = server.newPlatform.setup.core; - - const fieldFormatServiceFactory = async (uiSettings: IUiSettingsClient) => { - const [, plugins] = await coreSetup.getStartServices(); - const { fieldFormats } = (plugins as ReportingDeps).data; - - return fieldFormats.fieldFormatServiceFactory(uiSettings); - }; - - const __LEGACY: LegacySetup = { - config: server.config, - info: server.info, - route: server.route.bind(server), - plugins: { xpack_main: server.plugins.xpack_main }, - savedObjects: server.savedObjects, - fieldFormatServiceFactory, - uiSettingsServiceFactory: server.uiSettingsServiceFactory, - }; - - const plugin: ReportingPlugin = reportingPluginFactory( - server.newPlatform.coreContext, - __LEGACY, - this - ); - await plugin.setup(coreSetup, { - elasticsearch: coreSetup.elasticsearch, - security: server.newPlatform.setup.plugins.security as SecurityPluginSetup, - usageCollection: server.newPlatform.setup.plugins.usageCollection, - }); + return legacyInit(server, this); }, deprecations({ unused }: any) { diff --git a/x-pack/legacy/plugins/reporting/server/index.ts b/x-pack/legacy/plugins/reporting/server/index.ts new file mode 100644 index 0000000000000..438a3fd595a10 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from 'src/core/server'; +import { ReportingPlugin as Plugin } from './plugin'; + +export const plugin = (context: PluginInitializerContext) => { + return new Plugin(context); +}; diff --git a/x-pack/legacy/plugins/reporting/server/legacy.ts b/x-pack/legacy/plugins/reporting/server/legacy.ts new file mode 100644 index 0000000000000..c80aef06cf270 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/legacy.ts @@ -0,0 +1,49 @@ +/* + * 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 { Legacy } from 'kibana'; +import { PluginInitializerContext } from 'src/core/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; +import { ReportingPluginSpecOptions } from '../types'; +import { plugin } from './index'; +import { LegacySetup, ReportingStartDeps } from './plugin'; + +const buildLegacyDependencies = ( + server: Legacy.Server, + reportingPlugin: ReportingPluginSpecOptions +): LegacySetup => ({ + config: server.config, + info: server.info, + route: server.route.bind(server), + plugins: { + elasticsearch: server.plugins.elasticsearch, + xpack_main: server.plugins.xpack_main, + reporting: reportingPlugin, + }, + savedObjects: server.savedObjects, + uiSettingsServiceFactory: server.uiSettingsServiceFactory, +}); + +export const legacyInit = async ( + server: Legacy.Server, + reportingPlugin: ReportingPluginSpecOptions +) => { + const coreSetup = server.newPlatform.setup.core; + const pluginInstance = plugin(server.newPlatform.coreContext as PluginInitializerContext); + + await pluginInstance.setup(coreSetup, { + elasticsearch: coreSetup.elasticsearch, + security: server.newPlatform.setup.plugins.security as SecurityPluginSetup, + usageCollection: server.newPlatform.setup.plugins.usageCollection, + __LEGACY: buildLegacyDependencies(server, reportingPlugin), + }); + + // Schedule to call the "start" hook only after start dependencies are ready + coreSetup.getStartServices().then(([core, plugins]) => + pluginInstance.start(core, { + data: (plugins as ReportingStartDeps).data, + }) + ); +}; diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts index e618d23e8ed1f..ef7b01f8e9c15 100644 --- a/x-pack/legacy/plugins/reporting/server/plugin.ts +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -9,8 +9,8 @@ import { CoreSetup, CoreStart, ElasticsearchServiceSetup, - LoggerFactory, Plugin, + PluginInitializerContext, } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; @@ -24,95 +24,77 @@ import { ReportingPluginSpecOptions } from '../types.d'; import { createBrowserDriverFactory } from './browsers'; import { checkLicenseFactory, getExportTypesRegistry, LevelLogger, runValidations } from './lib'; import { registerRoutes } from './routes'; +import { setFieldFormats } from './services'; import { registerReportingUsageCollector } from './usage'; -export interface ReportingInitializerContext { - logger: LoggerFactory; -} - -// For now there is no exposed functionality to other plugins -export type ReportingSetup = object; -export type ReportingStart = object; - export interface ReportingSetupDeps { elasticsearch: ElasticsearchServiceSetup; usageCollection: UsageCollectionSetup; security: SecurityPluginSetup; + __LEGACY: LegacySetup; +} + +export interface ReportingStartDeps { + data: DataPluginStart; } -export type ReportingStartDeps = object; export interface LegacySetup { config: Legacy.Server['config']; info: Legacy.Server['info']; - plugins: { xpack_main: XPackMainPlugin & { status?: any } }; + plugins: { + elasticsearch: Legacy.Server['plugins']['elasticsearch']; + xpack_main: XPackMainPlugin & { + status?: any; + }; + reporting: ReportingPluginSpecOptions; + }; route: Legacy.Server['route']; savedObjects: Legacy.Server['savedObjects']; uiSettingsServiceFactory: Legacy.Server['uiSettingsServiceFactory']; - fieldFormatServiceFactory: DataPluginStart['fieldFormats']['fieldFormatServiceFactory']; } -export type ReportingPlugin = Plugin< - ReportingSetup, - ReportingStart, - ReportingSetupDeps, - ReportingStartDeps ->; - -/* We need a factory that returns an instance of the class because the class - * implementation itself restricts against having Legacy dependencies passed - * into `setup`. The factory parameters take the legacy dependencies, and the - * `setup` method gets it from enclosure */ -export function reportingPluginFactory( - initializerContext: ReportingInitializerContext, - __LEGACY: LegacySetup, - legacyPlugin: ReportingPluginSpecOptions -) { - return new (class ReportingPlugin implements ReportingPlugin { - private initializerContext: ReportingInitializerContext; - - constructor(context: ReportingInitializerContext) { - this.initializerContext = context; - } - - public async setup(core: CoreSetup, plugins: ReportingSetupDeps): Promise<ReportingSetup> { - const exportTypesRegistry = getExportTypesRegistry(); - const { usageCollection, elasticsearch } = plugins; - - let isCollectorReady = false; - // Register a function with server to manage the collection of usage stats - registerReportingUsageCollector( - usageCollection, - __LEGACY, - () => isCollectorReady, - exportTypesRegistry - ); - - const logger = new LevelLogger(this.initializerContext.logger.get('reporting')); - const browserDriverFactory = await createBrowserDriverFactory(__LEGACY, logger); - - logConfiguration(__LEGACY, logger); - runValidations(__LEGACY, elasticsearch, logger, browserDriverFactory); - - const { xpack_main: xpackMainPlugin } = __LEGACY.plugins; - mirrorPluginStatus(xpackMainPlugin, legacyPlugin); - const checkLicense = checkLicenseFactory(exportTypesRegistry); - (xpackMainPlugin as any).status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(PLUGIN_ID).registerLicenseCheckResultsGenerator(checkLicense); - }); - - // Post initialization of the above code, the collector is now ready to fetch its data - isCollectorReady = true; - - // Reporting routes - registerRoutes(__LEGACY, plugins, exportTypesRegistry, browserDriverFactory, logger); - - return {}; - } - - public start(core: CoreStart, plugins: ReportingStartDeps): ReportingStart { - return {}; - } - })(initializerContext); +export class ReportingPlugin implements Plugin<void, void, ReportingSetupDeps, ReportingStartDeps> { + constructor(private context: PluginInitializerContext) {} + + public async setup(core: CoreSetup, plugins: ReportingSetupDeps) { + const { elasticsearch, usageCollection, __LEGACY } = plugins; + const exportTypesRegistry = getExportTypesRegistry(); + + let isCollectorReady = false; + + // Register a function with server to manage the collection of usage stats + registerReportingUsageCollector( + usageCollection, + __LEGACY, + () => isCollectorReady, + exportTypesRegistry + ); + + const logger = new LevelLogger(this.context.logger.get('reporting')); + const browserDriverFactory = await createBrowserDriverFactory(__LEGACY, logger); + + logConfiguration(__LEGACY, logger); + runValidations(__LEGACY, elasticsearch, logger, browserDriverFactory); + + const { xpack_main: xpackMainPlugin, reporting } = __LEGACY.plugins; + mirrorPluginStatus(xpackMainPlugin, reporting); + + const checkLicense = checkLicenseFactory(exportTypesRegistry); + + (xpackMainPlugin as any).status.once('green', () => { + // Register a function that is called whenever the xpack info changes, + // to re-compute the license check results for this plugin + xpackMainPlugin.info.feature(PLUGIN_ID).registerLicenseCheckResultsGenerator(checkLicense); + }); + + // Post initialization of the above code, the collector is now ready to fetch its data + isCollectorReady = true; + + // Reporting routes + registerRoutes(__LEGACY, plugins, exportTypesRegistry, browserDriverFactory, logger); + } + + public start(core: CoreStart, plugins: ReportingStartDeps) { + setFieldFormats(plugins.data.fieldFormats); + } } diff --git a/x-pack/legacy/plugins/reporting/server/services.ts b/x-pack/legacy/plugins/reporting/server/services.ts new file mode 100644 index 0000000000000..7d15d2e1af1ae --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/services.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createGetterSetter } from '../../../../../src/plugins/kibana_utils/server'; +import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; + +export const [getFieldFormats, setFieldFormats] = createGetterSetter< + DataPluginStart['fieldFormats'] +>('FieldFormats'); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts index 7834bb4511dc6..19ad0d452feb1 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts @@ -5,7 +5,7 @@ */ import { IndexPatternMapping } from '../types'; -import { IndexPatternSavedObject } from '../../ml_popover/types'; +import { IndexPatternSavedObject } from '../../../hooks/types'; export const mockIndexPatternIds: IndexPatternMapping[] = [ { title: 'filebeat-*', id: '8c7323ac-97ad-4b53-ac0a-40f8f691a918' }, @@ -425,8 +425,7 @@ export const mockLayerListMixed = [ export const mockAPMIndexPattern: IndexPatternSavedObject = { id: 'apm-*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'apm-*', }, @@ -435,8 +434,7 @@ export const mockAPMIndexPattern: IndexPatternSavedObject = { export const mockAPMRegexIndexPattern: IndexPatternSavedObject = { id: 'apm-7.*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'apm-7.*', }, @@ -445,8 +443,7 @@ export const mockAPMRegexIndexPattern: IndexPatternSavedObject = { export const mockFilebeatIndexPattern: IndexPatternSavedObject = { id: 'filebeat-*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'filebeat-*', }, @@ -455,8 +452,7 @@ export const mockFilebeatIndexPattern: IndexPatternSavedObject = { export const mockAuditbeatIndexPattern: IndexPatternSavedObject = { id: 'auditbeat-*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'auditbeat-*', }, @@ -465,8 +461,7 @@ export const mockAuditbeatIndexPattern: IndexPatternSavedObject = { export const mockAPMTransactionIndexPattern: IndexPatternSavedObject = { id: 'apm-*-transaction*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'apm-*-transaction*', }, @@ -475,8 +470,7 @@ export const mockAPMTransactionIndexPattern: IndexPatternSavedObject = { export const mockGlobIndexPattern: IndexPatternSavedObject = { id: '*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: '*', }, diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx index 2d4714401f3b3..e370cbbf64a4a 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx @@ -21,7 +21,7 @@ import { getLayerList } from './map_config'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/common/constants'; import * as i18n from './translations'; import { Query, esFilters } from '../../../../../../../src/plugins/data/public'; -import { IndexPatternSavedObject } from '../ml_popover/types'; +import { IndexPatternSavedObject } from '../../hooks/types'; /** * Creates MapEmbeddable with provided initial configuration diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap index ee76657c8d27a..4dd77842894c6 100644 --- a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap @@ -3,25 +3,17 @@ exports[`Select Flow Direction rendering it renders the basic group button for uni-direction and bi-direction 1`] = ` <EuiFilterGroup> <EuiFilterButton - color="text" data-test-subj="uniDirectional" - grow={true} hasActiveFilters={true} - iconSide="right" onClick={[Function]} - type="button" withNext={true} > Unidirectional </EuiFilterButton> <EuiFilterButton - color="text" data-test-subj="biDirectional" - grow={true} hasActiveFilters={false} - iconSide="right" onClick={[Function]} - type="button" > Bidirectional </EuiFilterButton> diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts index 10b2538d1e785..cb84d9182d2e0 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - +import { npStart } from 'ui/new_platform'; import { Anomalies, InfluencerInput, CriteriaFields } from '../types'; import { throwIfNotOk } from '../../../hooks/api/api'; + export interface Body { jobIds: string[]; criteriaFields: CriteriaFields[]; @@ -22,17 +22,17 @@ export interface Body { } export const anomaliesTableData = async (body: Body, signal: AbortSignal): Promise<Anomalies> => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/results/anomalies_table_data`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify(body), - headers: { - 'content-Type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - signal, - }); - await throwIfNotOk(response); - return response.json(); + const response = await npStart.core.http.fetch<Anomalies>( + '/api/ml/results/anomalies_table_data', + { + method: 'POST', + body: JSON.stringify(body), + asResponse: true, + asSystemRequest: true, + signal, + } + ); + + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts index 1333951028494..dcfd7365f8e0d 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; import { InfluencerInput, MlCapabilities } from '../types'; import { throwIfNotOk } from '../../../hooks/api/api'; @@ -23,16 +23,13 @@ export interface Body { } export const getMlCapabilities = async (signal: AbortSignal): Promise<MlCapabilities> => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/ml_capabilities`, { + const response = await npStart.core.http.fetch<MlCapabilities>('/api/ml/ml_capabilities', { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-Type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, + asResponse: true, + asSystemRequest: true, signal, }); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx index a04b8f4b99653..cf939d8e09b7e 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; + import { CheckRecognizerProps, CloseJobsResponse, @@ -31,21 +32,18 @@ export const checkRecognizer = async ({ indexPatternName, signal, }: CheckRecognizerProps): Promise<RecognizerModule[]> => { - const response = await fetch( - `${chrome.getBasePath()}/api/ml/modules/recognize/${indexPatternName}`, + const response = await npStart.core.http.fetch<RecognizerModule[]>( + `/api/ml/modules/recognize/${indexPatternName}`, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, + asResponse: true, + asSystemRequest: true, signal, } ); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -55,18 +53,18 @@ export const checkRecognizer = async ({ * @param signal to cancel request */ export const getModules = async ({ moduleId = '', signal }: GetModulesProps): Promise<Module[]> => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/modules/get_module/${moduleId}`, { - method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - signal, - }); - await throwIfNotOk(response); - return response.json(); + const response = await npStart.core.http.fetch<Module[]>( + `/api/ml/modules/get_module/${moduleId}`, + { + method: 'GET', + asResponse: true, + asSystemRequest: true, + signal, + } + ); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -77,7 +75,6 @@ export const getModules = async ({ moduleId = '', signal }: GetModulesProps): Pr * @param jobIdErrorFilter - if provided, filters all errors except for given jobIds * @param groups - list of groups to add to jobs being installed * @param prefix - prefix to be added to job name - * @param headers optional headers to add */ export const setupMlJob = async ({ configTemplate, @@ -86,25 +83,26 @@ export const setupMlJob = async ({ groups = ['siem'], prefix = '', }: MlSetupArgs): Promise<SetupMlResponse> => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/modules/setup/${configTemplate}`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - prefix, - groups, - indexPatternName, - startDatafeed: false, - useDedicatedIndex: true, - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); - await throwIfNotOk(response); - const json = await response.json(); + const response = await npStart.core.http.fetch<SetupMlResponse>( + `/api/ml/modules/setup/${configTemplate}`, + { + method: 'POST', + body: JSON.stringify({ + prefix, + groups, + indexPatternName, + startDatafeed: false, + useDedicatedIndex: true, + }), + asResponse: true, + asSystemRequest: true, + } + ); + + await throwIfNotOk(response.response); + const json = response.body!; throwIfErrorAttachedToSetup(json, jobIdErrorFilter); + return json; }; @@ -121,22 +119,23 @@ export const startDatafeeds = async ({ datafeedIds: string[]; start: number; }): Promise<StartDatafeedResponse> => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/force_start_datafeeds`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - datafeedIds, - ...(start !== 0 && { start }), - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); - await throwIfNotOk(response); - const json = await response.json(); + const response = await npStart.core.http.fetch<StartDatafeedResponse>( + '/api/ml/jobs/force_start_datafeeds', + { + method: 'POST', + body: JSON.stringify({ + datafeedIds, + ...(start !== 0 && { start }), + }), + asResponse: true, + asSystemRequest: true, + } + ); + + await throwIfNotOk(response.response); + const json = response.body!; throwIfErrorAttached(json, datafeedIds); + return json; }; @@ -144,49 +143,46 @@ export const startDatafeeds = async ({ * Stops the given dataFeedIds and sets the corresponding Job's jobState to closed * * @param datafeedIds - * @param headers optional headers to add */ export const stopDatafeeds = async ({ datafeedIds, }: { datafeedIds: string[]; }): Promise<[StopDatafeedResponse | ErrorResponse, CloseJobsResponse]> => { - const stopDatafeedsResponse = await fetch(`${chrome.getBasePath()}/api/ml/jobs/stop_datafeeds`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - datafeedIds, - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); + const stopDatafeedsResponse = await npStart.core.http.fetch<StopDatafeedResponse>( + '/api/ml/jobs/stop_datafeeds', + { + method: 'POST', + body: JSON.stringify({ + datafeedIds, + }), + asResponse: true, + asSystemRequest: true, + } + ); - await throwIfNotOk(stopDatafeedsResponse); - const stopDatafeedsResponseJson = await stopDatafeedsResponse.json(); + await throwIfNotOk(stopDatafeedsResponse.response); + const stopDatafeedsResponseJson = stopDatafeedsResponse.body!; const datafeedPrefix = 'datafeed-'; - const closeJobsResponse = await fetch(`${chrome.getBasePath()}/api/ml/jobs/close_jobs`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - jobIds: datafeedIds.map(dataFeedId => - dataFeedId.startsWith(datafeedPrefix) - ? dataFeedId.substring(datafeedPrefix.length) - : dataFeedId - ), - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); + const closeJobsResponse = await npStart.core.http.fetch<CloseJobsResponse>( + '/api/ml/jobs/close_jobs', + { + method: 'POST', + body: JSON.stringify({ + jobIds: datafeedIds.map(dataFeedId => + dataFeedId.startsWith(datafeedPrefix) + ? dataFeedId.substring(datafeedPrefix.length) + : dataFeedId + ), + }), + asResponse: true, + asSystemRequest: true, + } + ); - await throwIfNotOk(closeJobsResponse); - return [stopDatafeedsResponseJson, await closeJobsResponse.json()]; + await throwIfNotOk(closeJobsResponse.response); + return [stopDatafeedsResponseJson, closeJobsResponse.body!]; }; /** @@ -198,17 +194,14 @@ export const stopDatafeeds = async ({ * @param signal to cancel request */ export const getJobsSummary = async (signal: AbortSignal): Promise<JobSummary[]> => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/jobs_summary`, { + const response = await npStart.core.http.fetch<JobSummary[]>('/api/ml/jobs/jobs_summary', { method: 'POST', - credentials: 'same-origin', body: JSON.stringify({}), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, + asResponse: true, + asSystemRequest: true, signal, }); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap index 4c9a27b76060c..8f40d0203afd4 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap @@ -5,16 +5,12 @@ exports[`GroupsFilterPopover renders correctly against snapshot 1`] = ` anchorPosition="downCenter" button={ <EuiFilterButton - color="text" data-test-subj="groups-filter-popover-button" - grow={true} hasActiveFilters={false} - iconSide="right" iconType="arrowDown" isSelected={false} numActiveFilters={0} onClick={[Function]} - type="button" > Groups </EuiFilterButton> diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap index fac91f75978f0..747ac63551b55 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap @@ -110,25 +110,17 @@ exports[`JobsTableFilters renders correctly against snapshot 1`] = ` > <EuiFilterGroup> <EuiFilterButton - color="text" data-test-subj="show-elastic-jobs-filter-button" - grow={true} hasActiveFilters={false} - iconSide="right" onClick={[Function]} - type="button" withNext={true} > Elastic jobs </EuiFilterButton> <EuiFilterButton - color="text" data-test-subj="show-custom-jobs-filter-button" - grow={true} hasActiveFilters={false} - iconSide="right" onClick={[Function]} - type="button" > Custom jobs </EuiFilterButton> diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts b/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts index 964ae8c8242d4..f3bf78fdbb94c 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts @@ -193,16 +193,6 @@ export interface CloseJobsResponse { }; } -export interface IndexPatternSavedObject { - attributes: { - title: string; - }; - id: string; - type: string; - updated_at: string; - version: string; -} - export interface JobsFilters { filterQuery: string; showCustomJobs: boolean; diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap index 7e3e099bf0276..28481e9970a5e 100644 --- a/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap @@ -3,7 +3,6 @@ exports[`Modal all errors rendering it renders the default all errors modal when isShowing is positive 1`] = ` <EuiOverlayMask> <EuiModal - maxWidth={true} onClose={[Function]} > <EuiModalHeader> diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index 8f4abeb31c226..4f50a9bd14788 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; + import { AddRulesProps, DeleteRulesProps, @@ -19,8 +20,9 @@ import { ImportRulesProps, ExportRulesProps, RuleError, - RuleStatus, + RuleStatusResponse, ImportRulesResponse, + PrePackagedRulesStatusResponse, } from './types'; import { throwIfNotOk } from '../../../hooks/api/api'; import { @@ -39,19 +41,15 @@ import * as i18n from '../../../pages/detection_engine/rules/translations'; * @param signal to cancel request */ export const addRule = async ({ rule, signal }: AddRulesProps): Promise<NewRule> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { + const response = await npStart.core.http.fetch<NewRule>(DETECTION_ENGINE_RULES_URL, { method: rule.id != null ? 'PUT' : 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify(rule), + asResponse: true, signal, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -79,40 +77,36 @@ export const fetchRules = async ({ signal, }: FetchRulesProps): Promise<FetchRulesResponse> => { const filters = [ - ...(filterOptions.filter.length !== 0 - ? [`alert.attributes.name:%20${encodeURIComponent(filterOptions.filter)}`] - : []), + ...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []), ...(filterOptions.showCustomRules - ? ['alert.attributes.tags:%20%22__internal_immutable:false%22'] + ? [`alert.attributes.tags: "__internal_immutable:false"`] : []), ...(filterOptions.showElasticRules - ? ['alert.attributes.tags:%20%22__internal_immutable:true%22'] + ? [`alert.attributes.tags: "__internal_immutable:true"`] : []), - ...(filterOptions.tags?.map(t => `alert.attributes.tags:${encodeURIComponent(t)}`) ?? []), + ...(filterOptions.tags?.map(t => `alert.attributes.tags: ${t}`) ?? []), ]; - const queryParams = [ - `page=${pagination.page}`, - `per_page=${pagination.perPage}`, - `sort_field=${filterOptions.sortField}`, - `sort_order=${filterOptions.sortOrder}`, - ...(filters.length > 0 ? [`filter=${filters.join('%20AND%20')}`] : []), - ]; + const query = { + page: pagination.page, + per_page: pagination.perPage, + sort_field: filterOptions.sortField, + sort_order: filterOptions.sortOrder, + ...(filters.length ? { filter: filters.join(' AND ') } : {}), + }; - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_find?${queryParams.join('&')}`, + const response = await npStart.core.http.fetch<FetchRulesResponse>( + `${DETECTION_ENGINE_RULES_URL}/_find`, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + query, signal, + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -123,18 +117,15 @@ export const fetchRules = async ({ * */ export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise<Rule> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id=${id}`, { + const response = await npStart.core.http.fetch<Rule>(DETECTION_ENGINE_RULES_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + query: { id }, + asResponse: true, signal, }); - await throwIfNotOk(response); - const rule: Rule = await response.json(); - return rule; + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -146,21 +137,17 @@ export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise<Rul * @throws An error if response is not OK */ export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise<Rule[]> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + const response = await npStart.core.http.fetch<Rule[]>( + `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, { method: 'PUT', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify(ids.map(id => ({ id, enabled }))), + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -171,21 +158,17 @@ export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise<R * @throws An error if response is not OK */ export const deleteRules = async ({ ids }: DeleteRulesProps): Promise<Array<Rule | RuleError>> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, + const response = await npStart.core.http.fetch<Rule[]>( + `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, { - method: 'DELETE', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + method: 'PUT', body: JSON.stringify(ids.map(id => ({ id }))), + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -194,15 +177,10 @@ export const deleteRules = async ({ ids }: DeleteRulesProps): Promise<Array<Rule * @param rules to duplicate */ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<Rule[]> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + const response = await npStart.core.http.fetch<Rule[]>( + `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, { method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify( rules.map(rule => ({ ...rule, @@ -223,11 +201,12 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<Ru status_date: undefined, })) ), + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -236,16 +215,13 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<Ru * @param signal AbortSignal for cancelling request */ export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise<boolean> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_PREPACKAGED_URL}`, { + const response = await npStart.core.http.fetch<unknown>(DETECTION_ENGINE_PREPACKAGED_URL, { method: 'PUT', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, }); - await throwIfNotOk(response); + + await throwIfNotOk(response.response); return true; }; @@ -266,21 +242,19 @@ export const importRules = async ({ const formData = new FormData(); formData.append('file', fileToImport); - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_import?overwrite=${overwrite}`, + const response = await npStart.core.http.fetch<ImportRulesResponse>( + `${DETECTION_ENGINE_RULES_URL}/_import`, { method: 'POST', - credentials: 'same-origin', - headers: { - 'kbn-xsrf': 'true', - }, + query: { overwrite }, body: formData, + asResponse: true, signal, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -304,24 +278,19 @@ export const exportRules = async ({ ? JSON.stringify({ objects: ruleIds.map(rule => ({ rule_id: rule })) }) : undefined; - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_export?exclude_export_details=${excludeExportDetails}&file_name=${encodeURIComponent( - filename - )}`, - { - method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - body, - signal, - } - ); + const response = await npStart.core.http.fetch<Blob>(`${DETECTION_ENGINE_RULES_URL}/_export`, { + method: 'POST', + body, + query: { + exclude_export_details: excludeExportDetails, + file_name: filename, + }, + signal, + asResponse: true, + }); - await throwIfNotOk(response); - return response.blob(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -338,24 +307,19 @@ export const getRuleStatusById = async ({ }: { id: string; signal: AbortSignal; -}): Promise<Record<string, RuleStatus>> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_STATUS_URL}?ids=${encodeURIComponent( - JSON.stringify([id]) - )}`, +}): Promise<RuleStatusResponse> => { + const response = await npStart.core.http.fetch<RuleStatusResponse>( + DETECTION_ENGINE_RULES_STATUS_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + query: { ids: JSON.stringify([id]) }, signal, + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -365,18 +329,14 @@ export const getRuleStatusById = async ({ * */ export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise<string[]> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_TAGS_URL}`, { + const response = await npStart.core.http.fetch<string[]>(DETECTION_ENGINE_TAGS_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -390,25 +350,16 @@ export const getPrePackagedRulesStatus = async ({ signal, }: { signal: AbortSignal; -}): Promise<{ - rules_custom_installed: number; - rules_installed: number; - rules_not_installed: number; - rules_not_updated: number; -}> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL}`, +}): Promise<PrePackagedRulesStatusResponse> => { + const response = await npStart.core.http.fetch<PrePackagedRulesStatusResponse>( + DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index b30c3b211b1b8..0aaffb7b86b28 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -197,3 +197,12 @@ export interface RuleInfoStatus { last_failure_message: string | null; last_success_message: string | null; } + +export type RuleStatusResponse = Record<string, RuleStatus>; + +export interface PrePackagedRulesStatusResponse { + rules_custom_installed: number; + rules_installed: number; + rules_not_installed: number; + rules_not_updated: number; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts index 8754d73637e7c..d0da70e646124 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; import { throwIfNotOk } from '../../../hooks/api/api'; import { @@ -14,40 +14,37 @@ import { DETECTION_ENGINE_PRIVILEGES_URL, } from '../../../../common/constants'; import { + BasicSignals, + PostSignalError, + Privilege, QuerySignals, + SignalIndexError, SignalSearchResponse, - UpdateSignalStatusProps, SignalsIndex, - SignalIndexError, - Privilege, - PostSignalError, - BasicSignals, + UpdateSignalStatusProps, } from './types'; -import { parseJsonFromBody } from '../../../utils/api'; /** * Fetch Signals by providing a query * * @param query String to match a dsl - * @param signal AbortSignal for cancelling request */ export const fetchQuerySignals = async <Hit, Aggregations>({ query, signal, }: QuerySignals): Promise<SignalSearchResponse<Hit, Aggregations>> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_QUERY_SIGNALS_URL}`, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - body: JSON.stringify(query), - signal, - }); - await throwIfNotOk(response); - const signals = await response.json(); - return signals; + const response = await npStart.core.http.fetch<SignalSearchResponse<Hit, Aggregations>>( + DETECTION_ENGINE_QUERY_SIGNALS_URL, + { + method: 'POST', + body: JSON.stringify(query), + asResponse: true, + signal, + } + ); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -62,19 +59,15 @@ export const updateSignalStatus = async ({ status, signal, }: UpdateSignalStatusProps): Promise<unknown> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_SIGNALS_STATUS_URL}`, { + const response = await npStart.core.http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify({ status, ...query }), + asResponse: true, signal, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -82,25 +75,18 @@ export const updateSignalStatus = async ({ * * @param signal AbortSignal for cancelling request */ -export const getSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex | null> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_INDEX_URL}`, { - method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - signal, - }); - if (response.ok) { - const signalIndex = await response.json(); - return signalIndex; +export const getSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex> => { + try { + return await npStart.core.http.fetch<SignalsIndex>(DETECTION_ENGINE_INDEX_URL, { + method: 'GET', + signal, + }); + } catch (e) { + if (e.body) { + throw new SignalIndexError(e.body); + } + throw e; } - const error = await parseJsonFromBody(response); - if (error != null) { - throw new SignalIndexError(error); - } - return null; }; /** @@ -108,19 +94,15 @@ export const getSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsI * * @param signal AbortSignal for cancelling request */ -export const getUserPrivilege = async ({ signal }: BasicSignals): Promise<Privilege | null> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_PRIVILEGES_URL}`, { +export const getUserPrivilege = async ({ signal }: BasicSignals): Promise<Privilege> => { + const response = await npStart.core.http.fetch<Privilege>(DETECTION_ENGINE_PRIVILEGES_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -128,23 +110,16 @@ export const getUserPrivilege = async ({ signal }: BasicSignals): Promise<Privil * * @param signal AbortSignal for cancelling request */ -export const createSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex | null> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_INDEX_URL}`, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - signal, - }); - if (response.ok) { - const signalIndex = await response.json(); - return signalIndex; - } - const error = await parseJsonFromBody(response); - if (error != null) { - throw new PostSignalError(error); +export const createSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex> => { + try { + return await npStart.core.http.fetch<SignalsIndex>(DETECTION_ENGINE_INDEX_URL, { + method: 'POST', + signal, + }); + } catch (e) { + if (e.body) { + throw new PostSignalError(e.body); + } + throw e; } - return null; }; diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx b/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx index 13f53cd34feb6..b12b04e8f760b 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx @@ -13,8 +13,7 @@ export const mockIndexPatternSavedObjects: IndexPatternSavedObject[] = [ attributes: { title: 'filebeat-*', }, - updated_at: '2019-08-26T04:30:09.111Z', - version: 'WzE4LLwxXQ==', + _version: 'WzE4LLwxXQ==', }, { type: 'index-pattern', @@ -22,7 +21,6 @@ export const mockIndexPatternSavedObjects: IndexPatternSavedObject[] = [ attributes: { title: 'auditbeat-*', }, - updated_at: '2019-08-26T04:31:12.934Z', - version: 'WzELLywxXQ==', + _version: 'WzELLywxXQ==', }, ]; diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts b/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts index 95825b7d4abda..208a3b14ca283 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts +++ b/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts @@ -13,6 +13,10 @@ describe('api', () => { }); describe('#throwIfNotOk', () => { + test('throws a network error if there is no response', async () => { + await expect(throwIfNotOk()).rejects.toThrow('Network Error'); + }); + test('does a throw if it is given response that is not ok and the body is not parsable', async () => { fetchMock.mock('http://example.com', 500); const response = await fetch('http://example.com'); diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx index 8d319ffe23902..f5f32da7d8c0b 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx @@ -4,46 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - +import { npStart } from 'ui/new_platform'; import * as i18n from '../translations'; import { parseJsonFromBody, ToasterErrors } from '../../components/ml/api/throw_if_not_ok'; -import { IndexPatternResponse, IndexPatternSavedObject } from '../types'; - -const emptyIndexPattern: IndexPatternSavedObject[] = []; +import { IndexPatternSavedObject, IndexPatternSavedObjectAttributes } from '../types'; /** * Fetches Configured Index Patterns from the Kibana saved objects API * * TODO: Refactor to context provider: https://github.com/elastic/siem-team/issues/448 - * - * @param signal */ -export const getIndexPatterns = async (signal: AbortSignal): Promise<IndexPatternSavedObject[]> => { - const response = await fetch( - `${chrome.getBasePath()}/api/saved_objects/_find?type=index-pattern&fields=title&fields=type&per_page=10000`, - { - method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - signal, - } - ); - await throwIfNotOk(response); - const results: IndexPatternResponse = await response.json(); +export const getIndexPatterns = async (): Promise<IndexPatternSavedObject[]> => { + const response = await npStart.core.savedObjects.client.find<IndexPatternSavedObjectAttributes>({ + type: 'index-pattern', + fields: ['title'], + perPage: 10000, + }); - if (results.saved_objects && Array.isArray(results.saved_objects)) { - return results.saved_objects; - } else { - return emptyIndexPattern; - } + return response.savedObjects; }; -export const throwIfNotOk = async (response: Response): Promise<void> => { +export const throwIfNotOk = async (response?: Response): Promise<void> => { + if (!response) { + throw new ToasterErrors([i18n.NETWORK_ERROR]); + } + if (!response.ok) { const body = await parseJsonFromBody(response); if (body != null && body.message) { diff --git a/x-pack/legacy/plugins/siem/public/hooks/types.ts b/x-pack/legacy/plugins/siem/public/hooks/types.ts index 4d66d8e191235..301b8bd655333 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/types.ts +++ b/x-pack/legacy/plugins/siem/public/hooks/types.ts @@ -4,19 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface IndexPatternSavedObject { - attributes: { - title: string; - }; - id: string; - type: string; - updated_at: string; - version: string; -} +import { SimpleSavedObject } from '../../../../../../src/core/public'; -export interface IndexPatternResponse { - page: number; - per_page: number; - saved_objects: IndexPatternSavedObject[]; - total: number; -} +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type IndexPatternSavedObjectAttributes = { title: string }; + +export type IndexPatternSavedObject = Pick< + SimpleSavedObject<IndexPatternSavedObjectAttributes>, + 'type' | 'id' | 'attributes' | '_version' +>; diff --git a/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx index 7abe88402096c..35bed69e8617e 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx @@ -8,10 +8,10 @@ import { useEffect, useState } from 'react'; import { useStateToaster } from '../components/toasters'; import { errorToToaster } from '../components/ml/api/error_to_toaster'; -import { IndexPatternSavedObject } from '../components/ml_popover/types'; -import { getIndexPatterns } from './api/api'; import * as i18n from './translations'; +import { IndexPatternSavedObject } from './types'; +import { getIndexPatterns } from './api/api'; type Return = [boolean, IndexPatternSavedObject[]]; @@ -22,12 +22,11 @@ export const useIndexPatterns = (refreshToggle = false): Return => { useEffect(() => { let isSubscribed = true; - const abortCtrl = new AbortController(); setIsLoading(true); async function fetchIndexPatterns() { try { - const data = await getIndexPatterns(abortCtrl.signal); + const data = await getIndexPatterns(); if (isSubscribed) { setIndexPatterns(data); @@ -44,7 +43,6 @@ export const useIndexPatterns = (refreshToggle = false): Return => { fetchIndexPatterns(); return () => { isSubscribed = false; - abortCtrl.abort(); }; }, [refreshToggle]); diff --git a/x-pack/legacy/plugins/siem/public/plugin.tsx b/x-pack/legacy/plugins/siem/public/plugin.tsx index 7911b5eb9833b..74fc913d2b573 100644 --- a/x-pack/legacy/plugins/siem/public/plugin.tsx +++ b/x-pack/legacy/plugins/siem/public/plugin.tsx @@ -41,6 +41,7 @@ export type Start = ReturnType<Plugin['start']>; export class Plugin implements IPlugin<Setup, Start> { public id = 'siem'; public name = 'SIEM'; + constructor( // @ts-ignore this is added to satisfy the New Platform typing constraint, // but we're not leveraging any of its functionality yet. diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts index dc308263baab6..8d00ddb18be6b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts @@ -35,32 +35,7 @@ describe('get_existing_prepackaged_rules', () => { expect(rules).toEqual([getResult()]); }); - test('should return 2 items over two pages, one per page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.params.immutable = true; - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.params.immutable = true; - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 }) - ); - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 }) - ); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getExistingPrepackagedRules({ - alertsClient: unsafeCast, - }); - expect(rules).toEqual([result1, result2]); - }); - - test('should return 3 items with over 3 pages one per page', async () => { + test('should return 3 items over 1 page with all on one page', async () => { const alertsClient = alertsClientMock.create(); const result1 = getResult(); @@ -75,40 +50,17 @@ describe('get_existing_prepackaged_rules', () => { result3.params.immutable = true; result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 }) + getFindResultWithMultiHits({ + data: [result1], + perPage: 1, + page: 1, + total: 3, + }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getExistingPrepackagedRules({ - alertsClient: unsafeCast, - }); - expect(rules).toEqual([result1, result2, result3]); - }); - - test('should return 3 items over 1 pages with all on one page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.params.immutable = true; - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.params.immutable = true; - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.params.immutable = true; - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - + // second mock which will return all the data on a single page alertsClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1, result2, result3], @@ -137,7 +89,7 @@ describe('get_existing_prepackaged_rules', () => { expect(rules).toEqual([getResult()]); }); - test('should return 2 items over two pages, one per page', async () => { + test('should return 2 items over 1 page', async () => { const alertsClient = alertsClientMock.create(); const result1 = getResult(); @@ -146,11 +98,19 @@ describe('get_existing_prepackaged_rules', () => { const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 }) + getFindResultWithMultiHits({ + data: [result1], + perPage: 1, + page: 1, + total: 2, + }) ); + + // second mock which will return all the data on a single page alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 }) + getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) ); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; @@ -160,7 +120,7 @@ describe('get_existing_prepackaged_rules', () => { expect(rules).toEqual([result1, result2]); }); - test('should return 3 items with over 3 pages one per page', async () => { + test('should return 3 items over 1 page with all on one page', async () => { const alertsClient = alertsClientMock.create(); const result1 = getResult(); @@ -172,37 +132,17 @@ describe('get_existing_prepackaged_rules', () => { const result3 = getResult(); result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 }) + getFindResultWithMultiHits({ + data: [result1], + perPage: 3, + page: 1, + total: 3, + }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRules({ - alertsClient: unsafeCast, - }); - expect(rules).toEqual([result1, result2, result3]); - }); - - test('should return 3 items over 1 pages with all on one page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - + // second mock which will return all the data on a single page alertsClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1, result2, result3], @@ -241,80 +181,27 @@ describe('get_existing_prepackaged_rules', () => { const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 }) - ); - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 }) - ); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRules({ - alertsClient: unsafeCast, - filter: '', - }); - expect(rules).toEqual([result1, result2]); - }); - - test('should return 3 items with over 3 pages one per page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 }) - ); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRules({ - alertsClient: unsafeCast, - filter: '', - }); - expect(rules).toEqual([result1, result2, result3]); - }); - - test('should return 3 items over 1 pages with all on one page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ - data: [result1, result2, result3], - perPage: 3, + data: [result1], + perPage: 1, page: 1, - total: 3, + total: 2, }) ); + // second mock which will return all the data on a single page + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) + ); + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rules = await getRules({ alertsClient: unsafeCast, filter: '', }); - expect(rules).toEqual([result1, result2, result3]); + expect(rules).toEqual([result1, result2]); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts index b7ab6a97634a8..a48957da7aa94 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts @@ -9,7 +9,6 @@ import { AlertsClient } from '../../../../../alerting'; import { RuleAlertType, isAlertTypes } from './types'; import { findRules } from './find_rules'; -export const DEFAULT_PER_PAGE = 100; export const FILTER_NON_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:false"`; export const FILTER_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`; @@ -33,84 +32,56 @@ export const getRulesCount = async ({ filter, perPage: 1, page: 1, + sortField: 'createdAt', + sortOrder: 'desc', }); return firstRule.total; }; export const getRules = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, filter, }: { alertsClient: AlertsClient; - perPage?: number; filter: string; }): Promise<RuleAlertType[]> => { - const firstPrepackedRules = await findRules({ + const count = await getRulesCount({ alertsClient, filter }); + const rules = await findRules({ alertsClient, filter, - perPage, + perPage: count, page: 1, + sortField: 'createdAt', + sortOrder: 'desc', }); - const totalPages = Math.ceil(firstPrepackedRules.total / firstPrepackedRules.perPage); - if (totalPages <= 1) { - if (isAlertTypes(firstPrepackedRules.data)) { - return firstPrepackedRules.data; - } else { - // If this was ever true, you have a really messed up system. - // This is keep typescript happy since we have an unknown with data - return []; - } - } else { - const returnPrepackagedRules = await Array(totalPages - 1) - .fill({}) - .map((_, page) => { - // page index starts at 2 as we already got the first page and we have more pages to go - return findRules({ - alertsClient, - filter, - perPage, - page: page + 2, - }); - }) - .reduce<Promise<object[]>>(async (accum, nextPage) => { - return [...(await accum), ...(await nextPage).data]; - }, Promise.resolve(firstPrepackedRules.data)); - if (isAlertTypes(returnPrepackagedRules)) { - return returnPrepackagedRules; - } else { - // If this was ever true, you have a really messed up system. - // This is keep typescript happy since we have an unknown with data - return []; - } + if (isAlertTypes(rules.data)) { + return rules.data; + } else { + // If this was ever true, you have a really messed up system. + // This is keep typescript happy since we have an unknown with data + return []; } }; export const getNonPackagedRules = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; - perPage?: number; }): Promise<RuleAlertType[]> => { return getRules({ alertsClient, - perPage, filter: FILTER_NON_PREPACKED_RULES, }); }; export const getExistingPrepackagedRules = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; - perPage?: number; }): Promise<RuleAlertType[]> => { return getRules({ alertsClient, - perPage, filter: FILTER_PREPACKED_RULES, }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts index 0f973d816917f..02456732df3b4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts @@ -9,8 +9,6 @@ import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; import { AlertsClient } from '../../../../../alerting'; import { findRules } from '../rules/find_rules'; -const DEFAULT_PER_PAGE: number = 1000; - export interface TagType { id: string; tags: string[]; @@ -42,39 +40,37 @@ export const convertTagsToSet = (tagObjects: object[]): Set<string> => { // Ref: https://www.elastic.co/guide/en/kibana/master/saved-objects-api.html export const readTags = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; - perPage?: number; }): Promise<string[]> => { - const tags = await readRawTags({ alertsClient, perPage }); + const tags = await readRawTags({ alertsClient }); return tags.filter(tag => !tag.startsWith(INTERNAL_IDENTIFIER)); }; export const readRawTags = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; perPage?: number; }): Promise<string[]> => { - const firstTags = await findRules({ alertsClient, fields: ['tags'], perPage, page: 1 }); - const firstSet = convertTagsToSet(firstTags.data); - const totalPages = Math.ceil(firstTags.total / firstTags.perPage); - if (totalPages <= 1) { - return Array.from(firstSet); - } else { - const returnTags = await Array(totalPages - 1) - .fill({}) - .map((_, page) => { - // page index starts at 2 as we already got the first page and we have more pages to go - return findRules({ alertsClient, fields: ['tags'], perPage, page: page + 2 }); - }) - .reduce<Promise<Set<string>>>(async (accum, nextTagPage) => { - const tagArray = convertToTags((await nextTagPage).data); - return new Set([...(await accum), ...tagArray]); - }, Promise.resolve(firstSet)); - - return Array.from(returnTags); - } + // Get just one record so we can get the total count + const firstTags = await findRules({ + alertsClient, + fields: ['tags'], + perPage: 1, + page: 1, + sortField: 'createdAt', + sortOrder: 'desc', + }); + // Get all the rules to aggregate over all the tags of the rules + const rules = await findRules({ + alertsClient, + fields: ['tags'], + perPage: firstTags.total, + sortField: 'createdAt', + sortOrder: 'desc', + page: 1, + }); + const tagSet = convertTagsToSet(rules.data); + return Array.from(tagSet); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/source_status/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/source_status/elasticsearch_adapter.ts index 17d203918d825..bb4693a7749e7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/source_status/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/source_status/elasticsearch_adapter.ts @@ -6,26 +6,54 @@ import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { SourceStatusAdapter } from './index'; +import { buildQuery } from './query.dsl'; +import { ApmServiceNameAgg } from './types'; + +const APM_INDEX_NAME = 'apm-*-transaction*'; export class ElasticsearchSourceStatusAdapter implements SourceStatusAdapter { constructor(private readonly framework: FrameworkAdapter) {} - public async hasIndices(request: FrameworkRequest, indexNames: string | string[]) { - return this.framework - .callWithRequest(request, 'search', { - index: indexNames, - size: 0, - terminate_after: 1, - allow_no_indices: true, - }) - .then( - response => response._shards.total > 0, - err => { - if (err.status === 404) { - return false; - } - throw err; - } - ); + public async hasIndices(request: FrameworkRequest, indexNames: string[]) { + // Intended flow to determine app-empty state is to first check siem indices (as this is a quick shard count), and + // if no shards exist only then perform the heavier APM query. This optimizes for normal use when siem data exists + try { + // Remove APM index if exists, and only query if length > 0 in case it's the only index provided + const nonApmIndexNames = indexNames.filter(name => name !== APM_INDEX_NAME); + const indexCheckResponse = await (nonApmIndexNames.length > 0 + ? this.framework.callWithRequest(request, 'search', { + index: nonApmIndexNames, + size: 0, + terminate_after: 1, + allow_no_indices: true, + }) + : Promise.resolve(undefined)); + + if ((indexCheckResponse?._shards.total ?? -1) > 0) { + return true; + } + + // Note: Additional check necessary for APM-specific index. For details see: https://github.com/elastic/kibana/issues/56363 + // Only verify if APM data exists if indexNames includes `apm-*-transaction*` (default included apm index) + const includesApmIndex = indexNames.includes(APM_INDEX_NAME); + const hasApmDataResponse = await (includesApmIndex + ? this.framework.callWithRequest<{}, ApmServiceNameAgg>( + request, + 'search', + buildQuery({ defaultIndex: [APM_INDEX_NAME] }) + ) + : Promise.resolve(undefined)); + + if ((hasApmDataResponse?.aggregations?.total_service_names?.value ?? -1) > 0) { + return true; + } + } catch (e) { + if (e.status === 404) { + return false; + } + throw e; + } + + return false; } } diff --git a/x-pack/legacy/plugins/siem/server/lib/source_status/query.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/source_status/query.dsl.ts new file mode 100644 index 0000000000000..ead08fff474c7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/source_status/query.dsl.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +const SERVICE_NAME = 'service.name'; + +export const buildQuery = ({ defaultIndex }: { defaultIndex: string[] }) => { + return { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + terminate_after: 1, + body: { + size: 0, + aggs: { + total_service_names: { + cardinality: { + field: SERVICE_NAME, + }, + }, + }, + }, + track_total_hits: false, + }; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/source_status/types.ts b/x-pack/legacy/plugins/siem/server/lib/source_status/types.ts new file mode 100644 index 0000000000000..247495ffe4b23 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/source_status/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface ApmServiceNameAgg { + total_service_names: { + value: number; + }; +} diff --git a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap index 8930dedfa0035..6e422bc13f06b 100644 --- a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap @@ -5,7 +5,6 @@ exports[`ConfirmDeleteModal renders as expected 1`] = ` <EuiModal className="spcConfirmDeleteModal" initialFocus="input[name=\\"confirmDeleteSpaceInput\\"]" - maxWidth={true} onClose={[MockFunction]} > <EuiModalHeader> diff --git a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx index 6eed58a784212..3a4861f4fbc9e 100644 --- a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx @@ -9,8 +9,6 @@ import { EuiButton, EuiButtonEmpty, EuiCallOut, - // @ts-ignore - EuiConfirmModal, EuiFieldText, EuiFormRow, EuiModal, @@ -89,7 +87,7 @@ class ConfirmDeleteModalUI extends Component<Props, State> { // This is largely the same as the built-in EuiConfirmModal component, but we needed the ability // to disable the buttons since this could be a long-running operation - const modalProps: EuiModalProps & CommonProps = { + const modalProps: Omit<EuiModalProps, 'children'> & CommonProps = { onClose: onCancel, className: 'spcConfirmDeleteModal', initialFocus: 'input[name="confirmDeleteSpaceInput"]', diff --git a/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap index d7702e2f18d44..750afcfc44e7e 100644 --- a/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap @@ -3,7 +3,6 @@ exports[`ConfirmAlterActiveSpaceModal renders as expected 1`] = ` <EuiOverlayMask> <EuiConfirmModal - buttonColor="primary" cancelButtonText="Cancel" confirmButtonText="Update space" defaultFocusedButton="confirm" diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap index 2d3351ec1c0d2..da9153f4a6c8d 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap @@ -6,26 +6,18 @@ exports[`FilterBar renders 1`] = ` > <EuiFilterGroup> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={false} - iconSide="right" key="all" numFilters={2} onClick={[Function]} - type="button" > all </EuiFilterButton> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={true} - iconSide="right" key="critical" numFilters={2} onClick={[Function]} - type="button" > critical </EuiFilterButton> diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap index b36e0c1a2bfdb..dfc69c57cfff6 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap @@ -6,24 +6,16 @@ exports[`GroupByBar renders 1`] = ` > <EuiFilterGroup> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={true} - iconSide="right" key="message" onClick={[Function]} - type="button" > by issue </EuiFilterButton> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={false} - iconSide="right" key="index" onClick={[Function]} - type="button" > by index </EuiFilterButton> diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts new file mode 100644 index 0000000000000..8924d07ac0c4d --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts @@ -0,0 +1,83 @@ +/* + * 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 { QueryContext } from '../query_context'; +import { CursorPagination } from '../..'; +import { CursorDirection, SortOrder } from '../../../../../../common/graphql/types'; + +describe(QueryContext, () => { + // 10 minute range + const rangeStart = '2019-02-03T19:06:54.939Z'; + const rangeEnd = '2019-02-03T19:16:54.939Z'; + + const pagination: CursorPagination = { + cursorDirection: CursorDirection.AFTER, + sortOrder: SortOrder.DESC, + }; + + let qc: QueryContext; + beforeEach(() => (qc = new QueryContext({}, rangeStart, rangeEnd, pagination, null, 10))); + + describe('dateRangeFilter()', () => { + const expectedRange = { + range: { + '@timestamp': { + gte: rangeStart, + lte: rangeEnd, + }, + }, + }; + describe('when hasTimespan() is true', () => { + it('should create a date range filter including the timespan', async () => { + const mockHasTimespan = jest.fn(); + mockHasTimespan.mockReturnValue(true); + qc.hasTimespan = mockHasTimespan; + + expect(await qc.dateRangeFilter()).toEqual({ + bool: { + filter: [ + expectedRange, + { + bool: { + should: [ + qc.timespanClause(), + { bool: { must_not: { exists: { field: 'monitor.timespan' } } } }, + ], + }, + }, + ], + }, + }); + }); + }); + + describe('when hasTimespan() is false', () => { + it('should only use the timestamp fields in the returned filter', async () => { + const mockHasTimespan = jest.fn(); + mockHasTimespan.mockReturnValue(false); + qc.hasTimespan = mockHasTimespan; + + expect(await qc.dateRangeFilter()).toEqual(expectedRange); + }); + }); + }); + + describe('timespanClause()', () => { + it('should always cover the last 5m', () => { + // 5m expected range between GTE and LTE in the response + // since timespan is hardcoded to 5m + expect(qc.timespanClause()).toEqual({ + range: { + 'monitor.timespan': { + // end date minus 5m + gte: new Date(Date.parse(rangeEnd) - 5 * 60 * 1000).toISOString(), + lte: rangeEnd, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts index 961cc94dcea19..a51931ba11630 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; import { APICaller } from 'kibana/server'; import { CursorPagination } from '../adapter_types'; import { INDEX_NAMES } from '../../../../../common/constants'; @@ -97,7 +98,7 @@ export class QueryContext { // behavior. const tsEnd = parseRelativeDate(this.dateRangeEnd, { roundUp: true })!; - const tsStart = tsEnd.subtract(5, 'minutes'); + const tsStart = moment(tsEnd).subtract(5, 'minutes'); return { range: { diff --git a/x-pack/package.json b/x-pack/package.json index ad0be351483f6..99e2a32bf3372 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -176,7 +176,7 @@ "@elastic/apm-rum-react": "^0.3.2", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "7.6.0", - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "@elastic/filesaver": "1.1.2", "@elastic/maki": "6.1.0", "@elastic/node-crypto": "^1.0.0", diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index 784125b83859d..fbd7404a2f15e 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -9,3 +9,10 @@ export interface ActionType { name: string; enabled: boolean; } + +export interface ActionResult { + id: string; + actionTypeId: string; + name: string; + config: Record<string, any>; +} diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index bb5d553b26b36..5399d13937179 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -84,7 +84,7 @@ It allows you to monitor the performance of thousands of applications in real ti '{config.docs.base_url}guide/en/apm/get-started/{config.docs.version}/index.html', }, }), - euiIconType: 'logoAPM', + euiIconType: 'apmApp', artifacts, onPrem: onPremInstructions(indices), elasticCloud: createElasticCloudInstructions(cloud), diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap index 42fd4417e238b..f8bbfbc8bb33d 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap @@ -49,7 +49,6 @@ exports[`APIKeysGridPage renders a callout when API keys are not enabled 1`] = ` width={16} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M7.59 10.059L7.35 5.18h1.3L8.4 10.06h-.81zm.394 1.901a.61.61 0 01-.448-.186.606.606 0 01-.186-.444c0-.174.062-.323.186-.446a.614.614 0 01.448-.184c.169 0 .315.06.44.182.124.122.186.27.186.448a.6.6 0 01-.189.446.607.607 0 01-.437.184zM2 14a1 1 0 01-.878-1.479l6-11a1 1 0 011.756 0l6 11A1 1 0 0114 14H2zm0-1h12L8 2 2 13z" fillRule="evenodd" @@ -189,7 +188,6 @@ exports[`APIKeysGridPage renders permission denied if user does not have require width={32} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M14 32l-.36-.14A21.07 21.07 0 010 12.07V5.44L14 .06l14 5.38v6.63a21.07 21.07 0 01-13.64 19.78L14 32zM2 6.82v5.25a19.08 19.08 0 0012 17.77 19.08 19.08 0 0012-17.77V6.82L14 2.2 2 6.82z" /> diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap index 37db2e118861e..a2741773f183b 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap @@ -39,6 +39,7 @@ exports[`<SimplePrivilegeForm> renders without crashing 1`] = ` fullWidth={false} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap index e9f2f946e9885..8d10e27df9694 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap @@ -176,6 +176,7 @@ exports[`<PrivilegeSpaceForm> renders without crashing 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap index 970cbfd03954a..4789314d9f780 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap @@ -67,7 +67,6 @@ exports[`<RolesGridPage /> renders permission denied if required 1`] = ` width={32} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M14 32l-.36-.14A21.07 21.07 0 010 12.07V5.44L14 .06l14 5.38v6.63a21.07 21.07 0 01-13.64 19.78L14 32zM2 6.82v5.25a19.08 19.08 0 0012 17.77 19.08 19.08 0 0012-17.77V6.82L14 2.2 2 6.82z" /> diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 47e11817ffa5d..3a6ba45413de6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1198,8 +1198,6 @@ "kbn.embeddable.inspectorRequestDataTitle": "データ", "kbn.embeddable.inspectorRequestDescription": "このリクエストは Elasticsearch にクエリをかけ、検索データを取得します。", "kbn.embeddable.search.displayName": "検索", - "kbn.home.addData.addDataToKibanaDescription": "これらのソリューションで、データを作成済みのダッシュボードと監視システムへとすぐに変えることができます。", - "kbn.home.addData.addDataToKibanaTitle": "Kibana にデータを追加", "kbn.home.addData.apm.addApmButtonLabel": "APM を追加", "kbn.home.addData.apm.nameDescription": "APM は、集約内から自動的に詳細なパフォーマンスメトリックやエラーを集めます。", "kbn.home.addData.apm.nameTitle": "APM", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 86d9a69dc0900..f7cbaa7d72158 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1198,8 +1198,6 @@ "kbn.embeddable.inspectorRequestDataTitle": "数据", "kbn.embeddable.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。", "kbn.embeddable.search.displayName": "搜索", - "kbn.home.addData.addDataToKibanaDescription": "使用这些解决方案可快速将您的数据转换成预建仪表板和监测系统。", - "kbn.home.addData.addDataToKibanaTitle": "将数据添加到 Kibana", "kbn.home.addData.apm.addApmButtonLabel": "添加 APM", "kbn.home.addData.apm.nameDescription": "APM 自动从您的应用程序内收集深入全面的性能指标和错误。", "kbn.home.addData.apm.nameTitle": "APM", diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts index aab683df09740..3da9552df976c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts @@ -108,6 +108,58 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it('should still be able to delete alert when AAD is broken', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + + await supertest + .put(`${getUrlPrefix(space.id)}/api/saved_objects/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + name: 'bar', + }, + }) + .expect(200); + + const response = await supertestWithoutAuth + .delete(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + objectRemover.add(space.id, createdAlert.id, 'alert'); + // Ensure task still exists + await getScheduledTask(createdAlert.scheduledTaskId); + break; + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + try { + await getScheduledTask(createdAlert.scheduledTaskId); + throw new Error('Should have removed scheduled task'); + } catch (e) { + expect(e.status).to.eql(404); + } + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/yarn.lock b/yarn.lock index a3acc2ae216c5..4b56ec6460775 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1953,10 +1953,10 @@ tabbable "^1.1.0" uuid "^3.1.0" -"@elastic/eui@18.2.1": - version "18.2.1" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-18.2.1.tgz#6ce6d0bd1d0541052d21f2918305524d71e91678" - integrity sha512-6C5tnWJTlBB++475i0vRoCsnz4JaYznb4zMNFLc+z5GY3vA3/E3AXTjmmBwybEicCCi3h1SnpJxZsgMakiZwRA== +"@elastic/eui@18.3.0": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-18.3.0.tgz#e21c6246624f694e2ae1c7c1f1a11b612faf260a" + integrity sha512-Rkj1rTtDa6iZMUF7pxYRojku1sLXzTU0FK1D9i0XE3H//exy3VyTV6qUlbdkiKXjO7emrgQqfzKDeXT+ZYztgg== dependencies: "@types/chroma-js" "^1.4.3" "@types/lodash" "^4.14.116"