diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index 650ef94e1d3da..bd55bd73966ff 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -28,13 +28,13 @@ def handleIngestion(timestamp) { kibanaCoverage.collectVcsInfo("### Collect VCS Info") kibanaCoverage.generateReports("### Merge coverage reports") kibanaCoverage.uploadCombinedReports() - kibanaCoverage.ingest(timestamp, '### Injest && Upload') + kibanaCoverage.ingest(env.JOB_NAME, BUILD_NUMBER, BUILD_URL, timestamp, '### Ingest && Upload') kibanaCoverage.uploadCoverageStaticSite(timestamp) } def handleFail() { def buildStatus = buildUtils.getBuildStatus() - if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED') { + if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED' && buildStatus != 'UNSTABLE') { slackNotifications.sendFailedBuild( channel: '#kibana-qa', username: 'Kibana QA' diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 47f9942162f75..bec0a0a33bad2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -84,7 +84,7 @@ /x-pack/plugins/infra/ @elastic/logs-metrics-ui /x-pack/plugins/ingest_manager/ @elastic/ingest-management /x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management -/x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest-management +/x-pack/plugins/observability/ @elastic/observability-ui /x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/uptime @elastic/uptime @@ -132,6 +132,7 @@ # Quality Assurance /src/dev/code_coverage @elastic/kibana-qa +/vars/*Coverage.groovy @elastic/kibana-qa /test/functional/services/common @elastic/kibana-qa /test/functional/services/lib @elastic/kibana-qa /test/functional/services/remote @elastic/kibana-qa diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md index fc141b8c89c18..498691c06285d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md @@ -7,7 +7,7 @@ Signature: ```typescript -SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "indicateNoData" | "timeHistory" | "onFiltersUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; } ``` diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index aaa46ab74714f..02b64157686c1 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -61,5 +61,8 @@ if (window.__kbnThemeVersion__ === 'v7') { ElasticEuiDarkTheme = require('@elastic/eui/dist/eui_theme_amsterdam_dark.json'); } +import * as Theme from './theme.ts'; +export { Theme }; + // massive deps that we should really get rid of or reduce in size substantially export const ElasticsearchBrowser = require('elasticsearch-browser/elasticsearch.js'); diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index 596c31820e80d..40e89f199b6a1 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -44,6 +44,7 @@ exports.externals = { 'react-router-dom': '__kbnSharedDeps__.ReactRouterDom', 'styled-components': '__kbnSharedDeps__.StyledComponents', '@kbn/monaco': '__kbnSharedDeps__.KbnMonaco', + '@kbn/ui-shared-deps/theme': '__kbnSharedDeps__.Theme', // this is how plugins/consumers from npm load monaco 'monaco-editor/esm/vs/editor/editor.api': '__kbnSharedDeps__.MonacoBarePluginApi', @@ -59,8 +60,8 @@ exports.externals = { '@elastic/eui/lib/services': '__kbnSharedDeps__.ElasticEuiLibServices', '@elastic/eui/lib/services/format': '__kbnSharedDeps__.ElasticEuiLibServicesFormat', '@elastic/eui/dist/eui_charts_theme': '__kbnSharedDeps__.ElasticEuiChartsTheme', - '@elastic/eui/dist/eui_theme_light.json': '__kbnSharedDeps__.ElasticEuiLightTheme', - '@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.ElasticEuiDarkTheme', + '@elastic/eui/dist/eui_theme_light.json': '__kbnSharedDeps__.Theme.euiLightVars', + '@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.Theme.euiDarkVars', /** * massive deps that we should really get rid of or reduce in size substantially diff --git a/packages/kbn-ui-shared-deps/theme.ts b/packages/kbn-ui-shared-deps/theme.ts new file mode 100644 index 0000000000000..ca4714779d39e --- /dev/null +++ b/packages/kbn-ui-shared-deps/theme.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 LightTheme from '@elastic/eui/dist/eui_theme_light.json'; + +const globals: any = typeof window === 'undefined' ? {} : window; + +export type Theme = typeof LightTheme; + +export let euiLightVars: Theme; +export let euiDarkVars: Theme; +if (globals.__kbnThemeVersion__ === 'v7') { + euiLightVars = require('@elastic/eui/dist/eui_theme_light.json'); + euiDarkVars = require('@elastic/eui/dist/eui_theme_dark.json'); +} else { + euiLightVars = require('@elastic/eui/dist/eui_theme_amsterdam_light.json'); + euiDarkVars = require('@elastic/eui/dist/eui_theme_amsterdam_dark.json'); +} + +/** + * EUI Theme vars that automatically adjust to light/dark theme + */ +export let euiThemeVars: Theme; +if (globals.__kbnDarkTheme__) { + euiThemeVars = euiDarkVars; +} else { + euiThemeVars = euiLightVars; +} diff --git a/packages/kbn-ui-shared-deps/tsconfig.json b/packages/kbn-ui-shared-deps/tsconfig.json index 5aa0f45e4100d..cef9a442d17bc 100644 --- a/packages/kbn-ui-shared-deps/tsconfig.json +++ b/packages/kbn-ui-shared-deps/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../../tsconfig.json", - "include": ["index.d.ts", "./monaco"] + "include": [ + "index.d.ts", + "theme.ts" + ] } diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index 831e1e55573b3..c81da4689052a 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -78,6 +78,17 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'], }, + { + include: [require.resolve('./theme.ts')], + use: [ + { + loader: 'babel-loader', + options: { + presets: [require.resolve('@kbn/babel-preset/webpack_preset')], + }, + }, + ], + }, ], }, diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 0fe3c1f083cf0..1b894bc400f08 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -185,27 +185,27 @@ export class ChromeService { /> ), }); + } - if (isIE()) { - notifications.toasts.addWarning({ - title: mountReactNode( - - - - ), - }} - /> - ), - }); - } + if (isIE()) { + notifications.toasts.addWarning({ + title: mountReactNode( + + + + ), + }} + /> + ), + }); } return { diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts index b88f5ba2c2b60..a80939a25ae65 100644 --- a/src/core/server/logging/integration_tests/logging.test.ts +++ b/src/core/server/logging/integration_tests/logging.test.ts @@ -18,6 +18,9 @@ */ import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import { InternalCoreSetup } from '../../internal_types'; +import { LoggerContextConfigInput } from '../logging_config'; +import { Subject } from 'rxjs'; function createRoot() { return kbnTestServer.createRoot({ @@ -111,4 +114,162 @@ describe('logging service', () => { expect(mockConsoleLog).toHaveBeenCalledTimes(0); }); }); + + describe('custom context configuration', () => { + const CUSTOM_LOGGING_CONFIG: LoggerContextConfigInput = { + appenders: { + customJsonConsole: { + kind: 'console', + layout: { + kind: 'json', + }, + }, + customPatternConsole: { + kind: 'console', + layout: { + kind: 'pattern', + pattern: 'CUSTOM - PATTERN [%logger][%level] %message', + }, + }, + }, + + loggers: [ + { context: 'debug_json', appenders: ['customJsonConsole'], level: 'debug' }, + { context: 'debug_pattern', appenders: ['customPatternConsole'], level: 'debug' }, + { context: 'info_json', appenders: ['customJsonConsole'], level: 'info' }, + { context: 'info_pattern', appenders: ['customPatternConsole'], level: 'info' }, + { + context: 'all', + appenders: ['customJsonConsole', 'customPatternConsole'], + level: 'debug', + }, + ], + }; + + let root: ReturnType; + let setup: InternalCoreSetup; + let mockConsoleLog: jest.SpyInstance; + const loggingConfig$ = new Subject(); + const setContextConfig = (enable: boolean) => + enable ? loggingConfig$.next(CUSTOM_LOGGING_CONFIG) : loggingConfig$.next({}); + beforeAll(async () => { + mockConsoleLog = jest.spyOn(global.console, 'log'); + root = kbnTestServer.createRoot(); + + setup = await root.setup(); + setup.logging.configure(['plugins', 'myplugin'], loggingConfig$); + }, 30000); + + beforeEach(() => { + mockConsoleLog.mockClear(); + }); + + afterAll(async () => { + mockConsoleLog.mockRestore(); + await root.shutdown(); + }); + + it('does not write to custom appenders when not configured', async () => { + const logger = root.logger.get('plugins.myplugin.debug_pattern'); + setContextConfig(false); + logger.info('log1'); + setContextConfig(true); + logger.debug('log2'); + logger.info('log3'); + setContextConfig(false); + logger.info('log4'); + expect(mockConsoleLog).toHaveBeenCalledTimes(2); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'CUSTOM - PATTERN [plugins.myplugin.debug_pattern][DEBUG] log2' + ); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'CUSTOM - PATTERN [plugins.myplugin.debug_pattern][INFO ] log3' + ); + }); + + it('writes debug_json context to custom JSON appender', async () => { + setContextConfig(true); + const logger = root.logger.get('plugins.myplugin.debug_json'); + logger.debug('log1'); + logger.info('log2'); + expect(mockConsoleLog).toHaveBeenCalledTimes(2); + + const [firstCall, secondCall] = mockConsoleLog.mock.calls.map(([jsonString]) => + JSON.parse(jsonString) + ); + expect(firstCall).toMatchObject({ + level: 'DEBUG', + context: 'plugins.myplugin.debug_json', + message: 'log1', + }); + expect(secondCall).toMatchObject({ + level: 'INFO', + context: 'plugins.myplugin.debug_json', + message: 'log2', + }); + }); + + it('writes info_json context to custom JSON appender', async () => { + setContextConfig(true); + const logger = root.logger.get('plugins.myplugin.info_json'); + logger.debug('i should not be logged!'); + logger.info('log2'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(1); + expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchObject({ + level: 'INFO', + context: 'plugins.myplugin.info_json', + message: 'log2', + }); + }); + + it('writes debug_pattern context to custom pattern appender', async () => { + setContextConfig(true); + const logger = root.logger.get('plugins.myplugin.debug_pattern'); + logger.debug('log1'); + logger.info('log2'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(2); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'CUSTOM - PATTERN [plugins.myplugin.debug_pattern][DEBUG] log1' + ); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'CUSTOM - PATTERN [plugins.myplugin.debug_pattern][INFO ] log2' + ); + }); + + it('writes info_pattern context to custom pattern appender', async () => { + setContextConfig(true); + const logger = root.logger.get('plugins.myplugin.info_pattern'); + logger.debug('i should not be logged!'); + logger.info('log2'); + expect(mockConsoleLog).toHaveBeenCalledTimes(1); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'CUSTOM - PATTERN [plugins.myplugin.info_pattern][INFO ] log2' + ); + }); + + it('writes all context to both appenders', async () => { + setContextConfig(true); + const logger = root.logger.get('plugins.myplugin.all'); + logger.debug('log1'); + logger.info('log2'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(4); + const logs = mockConsoleLog.mock.calls.map(([jsonString]) => jsonString); + + expect(JSON.parse(logs[0])).toMatchObject({ + level: 'DEBUG', + context: 'plugins.myplugin.all', + message: 'log1', + }); + expect(logs[1]).toEqual('CUSTOM - PATTERN [plugins.myplugin.all][DEBUG] log1'); + expect(JSON.parse(logs[2])).toMatchObject({ + level: 'INFO', + context: 'plugins.myplugin.all', + message: 'log2', + }); + expect(logs[3]).toEqual('CUSTOM - PATTERN [plugins.myplugin.all][INFO ] log2'); + }); + }); }); diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index c277dc85e5e04..46fd2b00c2304 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -51,7 +51,7 @@ const logger = loggingSystemMock.create(); expect.addSnapshotSerializer(createAbsolutePathSerializer()); -['path-1', 'path-2', 'path-3', 'path-4', 'path-5'].forEach((path) => { +['path-1', 'path-2', 'path-3', 'path-4', 'path-5', 'path-6', 'path-7', 'path-8'].forEach((path) => { jest.doMock(join(path, 'server'), () => ({}), { virtual: true, }); @@ -227,6 +227,26 @@ describe('PluginsService', () => { path: 'path-4', configPath: 'path-4-disabled', }), + createPlugin('plugin-with-disabled-optional-dep', { + path: 'path-5', + configPath: 'path-5', + optionalPlugins: ['explicitly-disabled-plugin'], + }), + createPlugin('plugin-with-missing-optional-dep', { + path: 'path-6', + configPath: 'path-6', + optionalPlugins: ['missing-plugin'], + }), + createPlugin('plugin-with-disabled-nested-transitive-dep', { + path: 'path-7', + configPath: 'path-7', + requiredPlugins: ['plugin-with-disabled-transitive-dep'], + }), + createPlugin('plugin-with-missing-nested-dep', { + path: 'path-8', + configPath: 'path-8', + requiredPlugins: ['plugin-with-missing-required-deps'], + }), ]), }); @@ -234,7 +254,7 @@ describe('PluginsService', () => { const setup = await pluginsService.setup(setupDeps); expect(setup.contracts).toBeInstanceOf(Map); - expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps); @@ -244,14 +264,20 @@ describe('PluginsService', () => { "Plugin \\"explicitly-disabled-plugin\\" is disabled.", ], Array [ - "Plugin \\"plugin-with-missing-required-deps\\" has been disabled since some of its direct or transitive dependencies are missing or disabled.", + "Plugin \\"plugin-with-missing-required-deps\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [missing-plugin]", ], Array [ - "Plugin \\"plugin-with-disabled-transitive-dep\\" has been disabled since some of its direct or transitive dependencies are missing or disabled.", + "Plugin \\"plugin-with-disabled-transitive-dep\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [another-explicitly-disabled-plugin]", ], Array [ "Plugin \\"another-explicitly-disabled-plugin\\" is disabled.", ], + Array [ + "Plugin \\"plugin-with-disabled-nested-transitive-dep\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [plugin-with-disabled-transitive-dep]", + ], + Array [ + "Plugin \\"plugin-with-missing-nested-dep\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [plugin-with-missing-required-deps]", + ], ] `); }); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 7441e753efa6a..5d1261e697bc0 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -239,11 +239,15 @@ export class PluginsService implements CoreService, parents: PluginName[] = [] - ): boolean { + ): { enabled: true } | { enabled: false; missingDependencies: string[] } { const pluginInfo = pluginEnableStatuses.get(pluginName); - return ( - pluginInfo !== undefined && - pluginInfo.isEnabled && - pluginInfo.plugin.requiredPlugins - .filter((dep) => !parents.includes(dep)) - .every((dependencyName) => - this.shouldEnablePlugin(dependencyName, pluginEnableStatuses, [...parents, pluginName]) - ) - ); + + if (pluginInfo === undefined || !pluginInfo.isEnabled) { + return { + enabled: false, + missingDependencies: [], + }; + } + + const missingDependencies = pluginInfo.plugin.requiredPlugins + .filter((dep) => !parents.includes(dep)) + .filter( + (dependencyName) => + !this.shouldEnablePlugin(dependencyName, pluginEnableStatuses, [...parents, pluginName]) + .enabled + ); + + if (missingDependencies.length === 0) { + return { + enabled: true, + }; + } + + return { + enabled: false, + missingDependencies, + }; } private registerPluginStaticDirs(deps: PluginsServiceSetupDeps) { diff --git a/src/core/server/ui_settings/saved_objects/ui_settings.ts b/src/core/server/ui_settings/saved_objects/ui_settings.ts index 26704f46a509c..452d1954b6e23 100644 --- a/src/core/server/ui_settings/saved_objects/ui_settings.ts +++ b/src/core/server/ui_settings/saved_objects/ui_settings.ts @@ -25,10 +25,7 @@ export const uiSettingsType: SavedObjectsType = { hidden: false, namespaceType: 'single', mappings: { - // we don't want to allow `true` in the public `SavedObjectsTypeMappingDefinition` type, however - // this is needed for the config that is kinda a special type. To avoid adding additional internal types - // just for this, we hardcast to any here. - dynamic: true as any, + dynamic: false, properties: { buildNum: { type: 'keyword', diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/ingest_helpers.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/ingest_helpers.test.js new file mode 100644 index 0000000000000..7ca7279e0d64c --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/__tests__/ingest_helpers.test.js @@ -0,0 +1,75 @@ +/* + * 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 expect from '@kbn/expect'; +import { maybeTeamAssign, whichIndex } from '../ingest_helpers'; +import { + TOTALS_INDEX, + RESEARCH_TOTALS_INDEX, + RESEARCH_COVERAGE_INDEX, + // COVERAGE_INDEX, +} from '../constants'; + +describe(`Ingest Helper fns`, () => { + describe(`whichIndex`, () => { + describe(`against the research job`, () => { + const whichIndexAgainstResearchJob = whichIndex(true); + describe(`against the totals index`, () => { + const isTotal = true; + it(`should return the Research Totals Index`, () => { + const actual = whichIndexAgainstResearchJob(isTotal); + expect(actual).to.be(RESEARCH_TOTALS_INDEX); + }); + }); + describe(`against the coverage index`, () => { + it(`should return the Research Totals Index`, () => { + const isTotal = false; + const actual = whichIndexAgainstResearchJob(isTotal); + expect(actual).to.be(RESEARCH_COVERAGE_INDEX); + }); + }); + }); + describe(`against the "prod" job`, () => { + const whichIndexAgainstProdJob = whichIndex(false); + describe(`against the totals index`, () => { + const isTotal = true; + it(`should return the "Prod" Totals Index`, () => { + const actual = whichIndexAgainstProdJob(isTotal); + expect(actual).to.be(TOTALS_INDEX); + }); + }); + }); + }); + describe(`maybeTeamAssign`, () => { + describe(`against a coverage index`, () => { + it(`should have the pipeline prop`, () => { + const actual = maybeTeamAssign(true, { a: 'blah' }); + expect(actual).to.have.property('pipeline'); + }); + }); + describe(`against a totals index`, () => { + describe(`for "prod"`, () => { + it(`should not have the pipeline prop`, () => { + const actual = maybeTeamAssign(false, { b: 'blah' }); + expect(actual).not.to.have.property('pipeline'); + }); + }); + }); + }); +}); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js index 8c982b792ed3b..2fd1d5cbe8d48 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js @@ -32,17 +32,33 @@ describe(`Transform fn`, () => { }); }); describe(`coveredFilePath`, () => { - it(`should remove the jenkins workspace path`, () => { - const obj = { - staticSiteUrl: - '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana/x-pack/plugins/reporting/server/browsers/extract/unzip.js', - COVERAGE_INGESTION_KIBANA_ROOT: - '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana', - }; - expect(coveredFilePath(obj)).to.have.property( - 'coveredFilePath', - 'x-pack/plugins/reporting/server/browsers/extract/unzip.js' - ); + describe(`in the code-coverage job`, () => { + it(`should remove the jenkins workspace path`, () => { + const obj = { + staticSiteUrl: + '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana/x-pack/plugins/reporting/server/browsers/extract/unzip.js', + COVERAGE_INGESTION_KIBANA_ROOT: + '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana', + }; + expect(coveredFilePath(obj)).to.have.property( + 'coveredFilePath', + 'x-pack/plugins/reporting/server/browsers/extract/unzip.js' + ); + }); + }); + describe(`in the qa research job`, () => { + it(`should remove the jenkins workspace path`, () => { + const obj = { + staticSiteUrl: + '/var/lib/jenkins/workspace/elastic+kibana+qa-research/kibana/x-pack/plugins/reporting/server/browsers/extract/unzip.js', + COVERAGE_INGESTION_KIBANA_ROOT: + '/var/lib/jenkins/workspace/elastic+kibana+qa-research/kibana', + }; + expect(coveredFilePath(obj)).to.have.property( + 'coveredFilePath', + 'x-pack/plugins/reporting/server/browsers/extract/unzip.js' + ); + }); }); }); describe(`itemizeVcs`, () => { diff --git a/src/dev/code_coverage/ingest_coverage/constants.js b/src/dev/code_coverage/ingest_coverage/constants.js index a7303f0778d1c..ddee7106f4490 100644 --- a/src/dev/code_coverage/ingest_coverage/constants.js +++ b/src/dev/code_coverage/ingest_coverage/constants.js @@ -18,4 +18,17 @@ */ export const COVERAGE_INDEX = process.env.COVERAGE_INDEX || 'kibana_code_coverage'; + export const TOTALS_INDEX = process.env.TOTALS_INDEX || `kibana_total_code_coverage`; + +export const RESEARCH_COVERAGE_INDEX = + process.env.RESEARCH_COVERAGE_INDEX || 'qa_research_code_coverage'; + +export const RESEARCH_TOTALS_INDEX = + process.env.RESEARCH_TOTALS_INDEX || `qa_research_total_code_coverage`; + +export const TEAM_ASSIGNMENT_PIPELINE_NAME = process.env.PIPELINE_NAME || 'team_assignment'; + +export const CODE_COVERAGE_CI_JOB_NAME = 'elastic+kibana+code-coverage'; +export const RESEARCH_CI_JOB_NAME = 'elastic+kibana+qa-research'; +export const CI_JOB_NAME = process.env.COVERAGE_JOB_NAME || RESEARCH_CI_JOB_NAME; diff --git a/src/dev/code_coverage/ingest_coverage/ingest.js b/src/dev/code_coverage/ingest_coverage/ingest.js index d6c55a9a655b8..43f0663ad0359 100644 --- a/src/dev/code_coverage/ingest_coverage/ingest.js +++ b/src/dev/code_coverage/ingest_coverage/ingest.js @@ -19,40 +19,77 @@ const { Client } = require('@elastic/elasticsearch'); import { createFailError } from '@kbn/dev-utils'; -import { COVERAGE_INDEX, TOTALS_INDEX } from './constants'; -import { errMsg, redact } from './ingest_helpers'; -import { noop } from './utils'; +import { RESEARCH_CI_JOB_NAME, TEAM_ASSIGNMENT_PIPELINE_NAME } from './constants'; +import { errMsg, redact, whichIndex } from './ingest_helpers'; +import { pretty, green } from './utils'; import { right, left } from './either'; const node = process.env.ES_HOST || 'http://localhost:9200'; + const client = new Client({ node }); -const pipeline = process.env.PIPELINE_NAME || 'team_assignment'; -const redacted = redact(node); +const redactedEsHostUrl = redact(node); +const parse = JSON.parse.bind(null); +const isResearchJob = process.env.COVERAGE_JOB_NAME === RESEARCH_CI_JOB_NAME ? true : false; export const ingest = (log) => async (body) => { - const index = body.isTotal ? TOTALS_INDEX : COVERAGE_INDEX; - const maybeWithPipeline = maybeTeamAssign(index, body); - const withIndex = { index, body: maybeWithPipeline }; - const dontSend = noop; - - log.verbose(withIndex); - - process.env.NODE_ENV === 'integration_test' - ? left(null) - : right(withIndex).fold(dontSend, async function doSend(finalPayload) { - await send(index, redacted, finalPayload); - }); + const isTotal = !!body.isTotal; + const index = whichIndex(isResearchJob)(isTotal); + const isACoverageIndex = isTotal ? false : true; + + const stringified = pretty(body); + const pipeline = TEAM_ASSIGNMENT_PIPELINE_NAME; + + const finalPayload = isACoverageIndex + ? { index, body: stringified, pipeline } + : { index, body: stringified }; + + const justLog = dontSendButLog(log); + const doSendToIndex = doSend(index); + const doSendRedacted = doSendToIndex(redactedEsHostUrl)(log)(client); + + eitherSendOrNot(finalPayload).fold(justLog, doSendRedacted); }; -async function send(idx, redacted, requestBody) { +function doSend(index) { + return (redactedEsHostUrl) => (log) => (client) => async (payload) => { + const logF = logSend(true)(redactedEsHostUrl)(log); + await send(logF, index, redactedEsHostUrl, client, payload); + }; +} + +function dontSendButLog(log) { + return (payload) => { + logSend(false)(null)(log)(payload); + }; +} + +async function send(logF, idx, redactedEsHostUrl, client, requestBody) { try { await client.index(requestBody); + logF(requestBody); } catch (e) { - throw createFailError(errMsg(idx, redacted, requestBody, e)); + const { body } = requestBody; + const parsed = parse(body); + throw createFailError(errMsg(idx, redactedEsHostUrl, parsed, e)); } } -export function maybeTeamAssign(index, body) { - const payload = index === TOTALS_INDEX ? body : { ...body, pipeline }; - return payload; +const sendMsg = (actuallySent, redactedEsHostUrl, payload) => { + const { index, body } = payload; + return `### ${actuallySent ? 'Sent' : 'Fake Sent'}: +${redactedEsHostUrl ? `\t### ES Host: ${redactedEsHostUrl}` : ''} +\t### Index: ${green(index)} +\t### payload.body: ${body} +${process.env.NODE_ENV === 'integration_test' ? `ingest-pipe=>${payload.pipeline}` : ''} +`; +}; + +function logSend(actuallySent) { + return (redactedEsHostUrl) => (log) => (payload) => { + log.verbose(sendMsg(actuallySent, redactedEsHostUrl, payload)); + }; +} + +function eitherSendOrNot(payload) { + return process.env.NODE_ENV === 'integration_test' ? left(payload) : right(payload); } diff --git a/src/dev/code_coverage/ingest_coverage/ingest_helpers.js b/src/dev/code_coverage/ingest_coverage/ingest_helpers.js index 11e5755bb0282..86bcf03977082 100644 --- a/src/dev/code_coverage/ingest_coverage/ingest_helpers.js +++ b/src/dev/code_coverage/ingest_coverage/ingest_helpers.js @@ -20,6 +20,13 @@ import { always, pretty } from './utils'; import chalk from 'chalk'; import { fromNullable } from './either'; +import { + COVERAGE_INDEX, + RESEARCH_COVERAGE_INDEX, + RESEARCH_TOTALS_INDEX, + TEAM_ASSIGNMENT_PIPELINE_NAME, + TOTALS_INDEX, +} from './constants'; export function errMsg(index, redacted, body, e) { const orig = fromNullable(e.body).fold( @@ -38,6 +45,9 @@ ${orig} ### Troubleshooting Hint: ${red('Perhaps the coverage data was not merged properly?\n')} + +### Error.meta (stringified): +${pretty(e.meta)} `; } @@ -59,3 +69,21 @@ function color(whichColor) { return chalk[whichColor].bgWhiteBright(x); }; } + +export function maybeTeamAssign(isACoverageIndex, body) { + const doAddTeam = isACoverageIndex ? true : false; + const payload = doAddTeam ? { ...body, pipeline: TEAM_ASSIGNMENT_PIPELINE_NAME } : body; + return payload; +} + +export function whichIndex(isResearchJob) { + return (isTotal) => + isTotal ? whichTotalsIndex(isResearchJob) : whichCoverageIndex(isResearchJob); +} +function whichTotalsIndex(isResearchJob) { + return isResearchJob ? RESEARCH_TOTALS_INDEX : TOTALS_INDEX; +} + +function whichCoverageIndex(isResearchJob) { + return isResearchJob ? RESEARCH_COVERAGE_INDEX : COVERAGE_INDEX; +} diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js index 013adc8b6b0af..2a65839f85ac3 100644 --- a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js @@ -47,7 +47,7 @@ describe('Ingesting coverage', () => { describe(`staticSiteUrl`, () => { let actualUrl = ''; - const siteUrlRegex = /staticSiteUrl:\s*(.+,)/; + const siteUrlRegex = /"staticSiteUrl":\s*(.+,)/; beforeAll(async () => { const opts = [...verboseArgs, resolved]; @@ -70,8 +70,8 @@ describe('Ingesting coverage', () => { }); describe(`vcsInfo`, () => { - let vcsInfo; describe(`without a commit msg in the vcs info file`, () => { + let vcsInfo; const args = [ 'scripts/ingest_coverage.js', '--verbose', @@ -93,9 +93,6 @@ describe('Ingesting coverage', () => { }); }); describe(`team assignment`, () => { - let shouldNotHavePipelineOut = ''; - let shouldIndeedHavePipelineOut = ''; - const args = [ 'scripts/ingest_coverage.js', '--verbose', @@ -104,28 +101,26 @@ describe('Ingesting coverage', () => { '--path', ]; - const teamAssignRE = /pipeline:/; - - beforeAll(async () => { - const summaryPath = 'jest-combined/coverage-summary-just-total.json'; - const resolved = resolve(MOCKS_DIR, summaryPath); - const opts = [...args, resolved]; - const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); - shouldNotHavePipelineOut = stdout; - }); - beforeAll(async () => { - const summaryPath = 'jest-combined/coverage-summary-manual-mix.json'; - const resolved = resolve(MOCKS_DIR, summaryPath); - const opts = [...args, resolved]; - const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); - shouldIndeedHavePipelineOut = stdout; - }); - - it(`should not occur when going to the totals index`, () => { - expect(teamAssignRE.test(shouldNotHavePipelineOut)).to.not.be.ok(); + it(`should not occur when going to the totals index`, async () => { + const teamAssignRE = /"pipeline":/; + const shouldNotHavePipelineOut = await prokJustTotalOrNot(true, args); + const actual = teamAssignRE.test(shouldNotHavePipelineOut); + expect(actual).to.not.be.ok(); }); - it(`should indeed occur when going to the coverage index`, () => { - expect(teamAssignRE.test(shouldIndeedHavePipelineOut)).to.be.ok(); + it(`should indeed occur when going to the coverage index`, async () => { + const shouldIndeedHavePipelineOut = await prokJustTotalOrNot(false, args); + const onlyForTestingRe = /ingest-pipe=>team_assignment/; + const actual = onlyForTestingRe.test(shouldIndeedHavePipelineOut); + expect(actual).to.be.ok(); }); }); }); +async function prokJustTotalOrNot(isTotal, args) { + const justTotalPath = 'jest-combined/coverage-summary-just-total.json'; + const notJustTotalPath = 'jest-combined/coverage-summary-manual-mix.json'; + + const resolved = resolve(MOCKS_DIR, isTotal ? justTotalPath : notJustTotalPath); + const opts = [...args, resolved]; + const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); + return stdout; +} diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-NO-total.json b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/jest-combined/coverage-summary-NO-total.json deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/src/dev/code_coverage/ingest_coverage/process.js b/src/dev/code_coverage/ingest_coverage/process.js index 6b9c8f09febfe..85a42cfffa6e2 100644 --- a/src/dev/code_coverage/ingest_coverage/process.js +++ b/src/dev/code_coverage/ingest_coverage/process.js @@ -36,13 +36,17 @@ import { import { resolve } from 'path'; import { createReadStream } from 'fs'; import readline from 'readline'; +import * as moment from 'moment'; const ROOT = '../../../..'; const COVERAGE_INGESTION_KIBANA_ROOT = process.env.COVERAGE_INGESTION_KIBANA_ROOT || resolve(__dirname, ROOT); const ms = process.env.DELAY || 0; const staticSiteUrlBase = process.env.STATIC_SITE_URL_BASE || 'https://kibana-coverage.elastic.dev'; -const addPrePopulatedTimeStamp = addTimeStamp(process.env.TIME_STAMP); +const format = 'YYYY-MM-DDTHH:mm:SS'; +// eslint-disable-next-line import/namespace +const formatted = `${moment.utc().format(format)}Z`; +const addPrePopulatedTimeStamp = addTimeStamp(process.env.TIME_STAMP || formatted); const preamble = pipe(statsAndstaticSiteUrl, rootDirAndOrigPath, buildId, addPrePopulatedTimeStamp); const addTestRunnerAndStaticSiteUrl = pipe(testRunner, staticSite(staticSiteUrlBase)); diff --git a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/ingest_coverage.sh index 2dae75484d68f..d3cf31fc0f427 100644 --- a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/ingest_coverage.sh @@ -3,11 +3,14 @@ echo "### Ingesting Code Coverage" echo "" +COVERAGE_JOB_NAME=$1 +export COVERAGE_JOB_NAME +echo "### debug COVERAGE_JOB_NAME: ${COVERAGE_JOB_NAME}" -BUILD_ID=$1 +BUILD_ID=$2 export BUILD_ID -CI_RUN_URL=$2 +CI_RUN_URL=$3 export CI_RUN_URL echo "### debug CI_RUN_URL: ${CI_RUN_URL}" @@ -17,6 +20,9 @@ export ES_HOST STATIC_SITE_URL_BASE='https://kibana-coverage.elastic.dev' export STATIC_SITE_URL_BASE +DELAY=100 +export DELAY + for x in jest functional; do echo "### Ingesting coverage for ${x}" diff --git a/src/dev/i18n/integrate_locale_files.test.ts b/src/dev/i18n/integrate_locale_files.test.ts index 7ff1d87f1bc55..3bd3dc61c044f 100644 --- a/src/dev/i18n/integrate_locale_files.test.ts +++ b/src/dev/i18n/integrate_locale_files.test.ts @@ -21,7 +21,7 @@ import { mockMakeDirAsync, mockWriteFileAsync } from './integrate_locale_files.t import path from 'path'; import { integrateLocaleFiles, verifyMessages } from './integrate_locale_files'; -// @ts-ignore +// @ts-expect-error import { normalizePath } from './utils'; const localePath = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files', 'fr.json'); @@ -36,6 +36,7 @@ const defaultIntegrateOptions = { sourceFileName: localePath, dryRun: false, ignoreIncompatible: false, + ignoreMalformed: false, ignoreMissing: false, ignoreUnused: false, config: { diff --git a/src/dev/i18n/integrate_locale_files.ts b/src/dev/i18n/integrate_locale_files.ts index d8ccccca15559..f9cd6dd1971c7 100644 --- a/src/dev/i18n/integrate_locale_files.ts +++ b/src/dev/i18n/integrate_locale_files.ts @@ -31,7 +31,8 @@ import { normalizePath, readFileAsync, writeFileAsync, - // @ts-ignore + verifyICUMessage, + // @ts-expect-error } from './utils'; import { I18nConfig } from './config'; @@ -41,6 +42,7 @@ export interface IntegrateOptions { sourceFileName: string; targetFileName?: string; dryRun: boolean; + ignoreMalformed: boolean; ignoreIncompatible: boolean; ignoreUnused: boolean; ignoreMissing: boolean; @@ -105,6 +107,23 @@ export function verifyMessages( } } + for (const messageId of localizedMessagesIds) { + const defaultMessage = defaultMessagesMap.get(messageId); + if (defaultMessage) { + try { + const message = localizedMessagesMap.get(messageId)!; + verifyICUMessage(message); + } catch (err) { + if (options.ignoreMalformed) { + localizedMessagesMap.delete(messageId); + options.log.warning(`Malformed translation ignored (${messageId}): ${err}`); + } else { + errorMessage += `\nMalformed translation (${messageId}): ${err}\n`; + } + } + } + } + if (errorMessage) { throw createFailError(errorMessage); } diff --git a/src/dev/i18n/tasks/check_compatibility.ts b/src/dev/i18n/tasks/check_compatibility.ts index 5900bf5aff252..afaf3cd875a8a 100644 --- a/src/dev/i18n/tasks/check_compatibility.ts +++ b/src/dev/i18n/tasks/check_compatibility.ts @@ -22,13 +22,14 @@ import { integrateLocaleFiles, I18nConfig } from '..'; export interface I18nFlags { fix: boolean; + ignoreMalformed: boolean; ignoreIncompatible: boolean; ignoreUnused: boolean; ignoreMissing: boolean; } export function checkCompatibility(config: I18nConfig, flags: I18nFlags, log: ToolingLog) { - const { fix, ignoreIncompatible, ignoreUnused, ignoreMissing } = flags; + const { fix, ignoreIncompatible, ignoreUnused, ignoreMalformed, ignoreMissing } = flags; return config.translations.map((translationsPath) => ({ task: async ({ messages }: { messages: Map }) => { // If `fix` is set we should try apply all possible fixes and override translations file. @@ -37,6 +38,7 @@ export function checkCompatibility(config: I18nConfig, flags: I18nFlags, log: To ignoreIncompatible: fix || ignoreIncompatible, ignoreUnused: fix || ignoreUnused, ignoreMissing: fix || ignoreMissing, + ignoreMalformed: fix || ignoreMalformed, sourceFileName: translationsPath, targetFileName: fix ? translationsPath : undefined, config, diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index 1d1c3118e0852..11a002fdbf4a8 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -208,6 +208,28 @@ export function checkValuesProperty(prefixedValuesKeys, defaultMessage, messageI } } +/** + * Verifies valid ICU message. + * @param message ICU message. + * @param messageId ICU message id + * @returns {undefined} + */ +export function verifyICUMessage(message) { + try { + parser.parse(message); + } catch (error) { + if (error.name === 'SyntaxError') { + const errorWithContext = createParserErrorMessage(message, { + loc: { + line: error.location.start.line, + column: error.location.start.column - 1, + }, + message: error.message, + }); + throw errorWithContext; + } + } +} /** * Extracts value references from the ICU message. * @param message ICU message. diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 391a52b7f0397..e11668ab57f55 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -84,7 +84,7 @@ export default { moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], modulePathIgnorePatterns: ['__fixtures__/', 'target/'], testEnvironment: 'jest-environment-jsdom-thirteen', - testMatch: ['**/*.test.{js,ts,tsx}'], + testMatch: ['**/*.test.{js,mjs,ts,tsx}'], testPathIgnorePatterns: [ '/packages/kbn-ui-framework/(dist|doc_site|generator-kui)/', '/packages/kbn-pm/dist/', diff --git a/src/dev/run_i18n_check.ts b/src/dev/run_i18n_check.ts index 97ea988b1de3a..70eeedac2b8b6 100644 --- a/src/dev/run_i18n_check.ts +++ b/src/dev/run_i18n_check.ts @@ -36,6 +36,7 @@ run( async ({ flags: { 'ignore-incompatible': ignoreIncompatible, + 'ignore-malformed': ignoreMalformed, 'ignore-missing': ignoreMissing, 'ignore-unused': ignoreUnused, 'include-config': includeConfig, @@ -48,12 +49,13 @@ run( fix && (ignoreIncompatible !== undefined || ignoreUnused !== undefined || + ignoreMalformed !== undefined || ignoreMissing !== undefined) ) { throw createFailError( `${chalk.white.bgRed( ' I18N ERROR ' - )} none of the --ignore-incompatible, --ignore-unused or --ignore-missing is allowed when --fix is set.` + )} none of the --ignore-incompatible, --ignore-malformed, --ignore-unused or --ignore-missing is allowed when --fix is set.` ); } @@ -99,6 +101,7 @@ run( checkCompatibility( config, { + ignoreMalformed: !!ignoreMalformed, ignoreIncompatible: !!ignoreIncompatible, ignoreUnused: !!ignoreUnused, ignoreMissing: !!ignoreMissing, diff --git a/src/dev/run_i18n_integrate.ts b/src/dev/run_i18n_integrate.ts index 23d66fae9f26e..25c3ea32783aa 100644 --- a/src/dev/run_i18n_integrate.ts +++ b/src/dev/run_i18n_integrate.ts @@ -31,6 +31,7 @@ run( 'ignore-incompatible': ignoreIncompatible = false, 'ignore-missing': ignoreMissing = false, 'ignore-unused': ignoreUnused = false, + 'ignore-malformed': ignoreMalformed = false, 'include-config': includeConfig, path, source, @@ -66,12 +67,13 @@ run( typeof ignoreIncompatible !== 'boolean' || typeof ignoreUnused !== 'boolean' || typeof ignoreMissing !== 'boolean' || + typeof ignoreMalformed !== 'boolean' || typeof dryRun !== 'boolean' ) { throw createFailError( `${chalk.white.bgRed( ' I18N ERROR ' - )} --ignore-incompatible, --ignore-unused, --ignore-missing, and --dry-run can't have values` + )} --ignore-incompatible, --ignore-unused, --ignore-malformed, --ignore-missing, and --dry-run can't have values` ); } @@ -97,6 +99,7 @@ run( ignoreIncompatible, ignoreUnused, ignoreMissing, + ignoreMalformed, config, log, }); diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js index 17610702a0bc7..30e7587707d2e 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js @@ -66,10 +66,9 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../../plugins/vis_type_vega/public/services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setInjectedVarFunc } from '../../../../../../plugins/maps_legacy/public/kibana_services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceSettings } from '../../../../../../plugins/maps_legacy/public/map/service_settings'; -import { getKibanaMapFactoryProvider } from '../../../../../../plugins/maps_legacy/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaMap } from '../../../../../../plugins/maps_legacy/public/map/kibana_map'; const THRESHOLD = 0.1; const PIXEL_DIFF = 30; @@ -82,18 +81,7 @@ describe('VegaVisualizations', () => { let vegaVisualizationDependencies; let vegaVisType; - const coreSetupMock = { - notifications: { - toasts: {}, - }, - uiSettings: { - get: () => {}, - }, - injectedMetadata: { - getInjectedVar: () => {}, - }, - }; - setKibanaMapFactory(getKibanaMapFactoryProvider(coreSetupMock)); + setKibanaMapFactory((...args) => new KibanaMap(...args)); setInjectedVars({ emsTileLayerId: {}, enableExternalUrls: true, @@ -139,30 +127,6 @@ describe('VegaVisualizations', () => { beforeEach(ngMock.module('kibana')); beforeEach( ngMock.inject(() => { - setInjectedVarFunc((injectedVar) => { - switch (injectedVar) { - case 'mapConfig': - return { - emsFileApiUrl: '', - emsTileApiUrl: '', - emsLandingPageUrl: '', - }; - case 'tilemapsConfig': - return { - deprecated: { - config: { - options: { - attribution: '123', - }, - }, - }, - }; - case 'version': - return '123'; - default: - return 'not found'; - } - }); const serviceSettings = new ServiceSettings(mockMapConfig, mockMapConfig.tilemap); vegaVisualizationDependencies = { serviceSettings, diff --git a/src/plugins/console/public/plugin.ts b/src/plugins/console/public/plugin.ts index 49a232ce35cd0..851dc7a063d7b 100644 --- a/src/plugins/console/public/plugin.ts +++ b/src/plugins/console/public/plugin.ts @@ -18,17 +18,13 @@ */ import { i18n } from '@kbn/i18n'; - -import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { Plugin, CoreSetup } from 'src/core/public'; import { FeatureCatalogueCategory } from '../../home/public'; - import { AppSetupUIPluginDependencies } from './types'; export class ConsoleUIPlugin implements Plugin { - constructor() {} - - async setup( + public setup( { notifications, getStartServices }: CoreSetup, { devTools, home, usageCollection }: AppSetupUIPluginDependencies ) { @@ -53,16 +49,25 @@ export class ConsoleUIPlugin implements Plugin { + mount: async ({ element }) => { + const [core] = await getStartServices(); + + const { + injectedMetadata, + i18n: { Context: I18nContext }, + docLinks: { DOC_LINK_VERSION }, + } = core; + const { renderApp } = await import('./application'); - const [{ injectedMetadata }] = await getStartServices(); + const elasticsearchUrl = injectedMetadata.getInjectedVar( 'elasticsearchUrl', 'http://localhost:9200' ) as string; + return renderApp({ - docLinkVersion: docLinks.DOC_LINK_VERSION, - I18nContext: i18nDep.Context, + docLinkVersion: DOC_LINK_VERSION, + I18nContext, notifications, elasticsearchUrl, usageCollection, @@ -72,5 +77,5 @@ export class ConsoleUIPlugin implements Plugin = event.table.columns[event.column]; if (!column || !column.meta) { diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts index a0e285c20d776..3e38477a908b8 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts @@ -27,7 +27,7 @@ import { dataPluginMock } from '../../../public/mocks'; import { setIndexPatterns } from '../../../public/services'; import { mockDataServices } from '../../../public/search/aggs/test_helpers'; import { createFiltersFromValueClickAction } from './create_filters_from_value_click'; -import { ValueClickTriggerContext } from '../../../../embeddable/public'; +import { ValueClickContext } from '../../../../embeddable/public'; const mockField = { name: 'bytes', @@ -39,7 +39,7 @@ const mockField = { }; describe('createFiltersFromValueClick', () => { - let dataPoints: ValueClickTriggerContext['data']['data']; + let dataPoints: ValueClickContext['data']['data']; beforeEach(() => { dataPoints = [ diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts index 2fdd746535519..1974b9f776748 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts @@ -21,7 +21,7 @@ import { KibanaDatatable } from '../../../../../plugins/expressions/public'; import { deserializeAggConfig } from '../../search/expressions'; import { esFilters, Filter } from '../../../public'; import { getIndexPatterns } from '../../../public/services'; -import { ValueClickTriggerContext } from '../../../../embeddable/public'; +import { ValueClickContext } from '../../../../embeddable/public'; /** * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter @@ -114,7 +114,7 @@ const createFilter = async ( export const createFiltersFromValueClickAction = async ({ data, negate, -}: ValueClickTriggerContext['data']) => { +}: ValueClickContext['data']) => { const filters: Filter[] = []; await Promise.all( diff --git a/src/plugins/data/public/actions/select_range_action.ts b/src/plugins/data/public/actions/select_range_action.ts index 18853f7e292f6..49766143b5588 100644 --- a/src/plugins/data/public/actions/select_range_action.ts +++ b/src/plugins/data/public/actions/select_range_action.ts @@ -24,12 +24,12 @@ import { ActionByType, } from '../../../../plugins/ui_actions/public'; import { createFiltersFromRangeSelectAction } from './filters/create_filters_from_range_select'; -import { RangeSelectTriggerContext } from '../../../embeddable/public'; +import { RangeSelectContext } from '../../../embeddable/public'; import { FilterManager, TimefilterContract, esFilters } from '..'; export const ACTION_SELECT_RANGE = 'ACTION_SELECT_RANGE'; -export type SelectRangeActionContext = RangeSelectTriggerContext; +export type SelectRangeActionContext = RangeSelectContext; async function isCompatible(context: SelectRangeActionContext) { try { diff --git a/src/plugins/data/public/actions/value_click_action.ts b/src/plugins/data/public/actions/value_click_action.ts index 5d4f1f5f1d6db..dd74a7ee507f3 100644 --- a/src/plugins/data/public/actions/value_click_action.ts +++ b/src/plugins/data/public/actions/value_click_action.ts @@ -27,12 +27,12 @@ import { import { getOverlays, getIndexPatterns } from '../services'; import { applyFiltersPopover } from '../ui/apply_filters'; import { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click'; -import { ValueClickTriggerContext } from '../../../embeddable/public'; +import { ValueClickContext } from '../../../embeddable/public'; import { Filter, FilterManager, TimefilterContract, esFilters } from '..'; export const ACTION_VALUE_CLICK = 'ACTION_VALUE_CLICK'; -export type ValueClickActionContext = ValueClickTriggerContext; +export type ValueClickActionContext = ValueClickContext; async function isCompatible(context: ValueClickActionContext) { try { diff --git a/src/plugins/data/public/index_patterns/expressions/index.ts b/src/plugins/data/public/index_patterns/expressions/index.ts new file mode 100644 index 0000000000000..fa37e3b216ac9 --- /dev/null +++ b/src/plugins/data/public/index_patterns/expressions/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './load_index_pattern'; diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/ingest.test.js b/src/plugins/data/public/index_patterns/expressions/load_index_pattern.test.ts similarity index 56% rename from src/dev/code_coverage/ingest_coverage/__tests__/ingest.test.js rename to src/plugins/data/public/index_patterns/expressions/load_index_pattern.test.ts index ad5b4da0873b9..378ceb376f5f1 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/ingest.test.js +++ b/src/plugins/data/public/index_patterns/expressions/load_index_pattern.test.ts @@ -17,21 +17,23 @@ * under the License. */ -import expect from '@kbn/expect'; -import { maybeTeamAssign } from '../ingest'; -import { COVERAGE_INDEX, TOTALS_INDEX } from '../constants'; +import { indexPatternLoad } from './load_index_pattern'; -describe(`Ingest fns`, () => { - describe(`maybeTeamAssign fn`, () => { - describe(`against the coverage index`, () => { - it(`should have the pipeline prop`, () => { - expect(maybeTeamAssign(COVERAGE_INDEX, {})).to.have.property('pipeline'); - }); - }); - describe(`against the totals index`, () => { - it(`should not have the pipeline prop`, () => { - expect(maybeTeamAssign(TOTALS_INDEX, {})).not.to.have.property('pipeline'); - }); - }); +jest.mock('../../services', () => ({ + getIndexPatterns: () => ({ + get: (id: string) => ({ + toSpec: () => ({ + title: 'value', + }), + }), + }), +})); + +describe('indexPattern expression function', () => { + test('returns serialized index pattern', async () => { + const indexPatternDefinition = indexPatternLoad(); + const result = await indexPatternDefinition.fn(null, { id: '1' }, {} as any); + expect(result.type).toEqual('index_pattern'); + expect(result.value.title).toEqual('value'); }); }); diff --git a/src/plugins/data/public/index_patterns/expressions/load_index_pattern.ts b/src/plugins/data/public/index_patterns/expressions/load_index_pattern.ts new file mode 100644 index 0000000000000..901d6aac7fbff --- /dev/null +++ b/src/plugins/data/public/index_patterns/expressions/load_index_pattern.ts @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../../plugins/expressions/public'; +import { getIndexPatterns } from '../../services'; +import { IndexPatternSpec } from '../../../common/index_patterns'; + +const name = 'indexPatternLoad'; + +type Input = null; +type Output = Promise<{ type: 'index_pattern'; value: IndexPatternSpec }>; + +interface Arguments { + id: string; +} + +export const indexPatternLoad = (): ExpressionFunctionDefinition< + typeof name, + Input, + Arguments, + Output +> => ({ + name, + type: 'index_pattern', + inputTypes: ['null'], + help: i18n.translate('data.functions.indexPatternLoad.help', { + defaultMessage: 'Loads an index pattern', + }), + args: { + id: { + types: ['string'], + required: true, + help: i18n.translate('data.functions.indexPatternLoad.id.help', { + defaultMessage: 'index pattern id to load', + }), + }, + }, + async fn(input, args) { + const indexPatterns = getIndexPatterns(); + + const indexPattern = await indexPatterns.get(args.id); + + return { type: 'index_pattern', value: indexPattern.toSpec() }; + }, +}); diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index d5929cb9cd564..ec71794fde87d 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -82,6 +82,7 @@ import { ValueClickActionContext, } from './actions/value_click_action'; import { SavedObjectsClientPublicToCommon } from './index_patterns'; +import { indexPatternLoad } from './index_patterns/expressions/load_index_pattern'; declare module '../../ui_actions/public' { export interface ActionContextMapping { @@ -126,6 +127,7 @@ export class DataPublicPlugin implements Plugin, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "indicateNoData" | "timeHistory" | "onFiltersUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; }; // Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/tabify/get_columns.test.ts b/src/plugins/data/public/search/tabify/get_columns.test.ts index 0c5551d95690f..35f0181f63302 100644 --- a/src/plugins/data/public/search/tabify/get_columns.test.ts +++ b/src/plugins/data/public/search/tabify/get_columns.test.ts @@ -161,4 +161,20 @@ describe('get columns', () => { 'Sum of @timestamp', ]); }); + + test('should not fail if there is no field for date histogram agg', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: {}, + }, + { type: 'sum', schema: 'metric', params: { field: '@timestamp' } }, + ]).aggs, + false + ); + + expect(columns.map((c) => c.name)).toEqual(['', 'Sum of @timestamp']); + }); }); diff --git a/src/plugins/data/public/search/tabify/get_columns.ts b/src/plugins/data/public/search/tabify/get_columns.ts index 8c538288d2fea..8e907d4b0cb88 100644 --- a/src/plugins/data/public/search/tabify/get_columns.ts +++ b/src/plugins/data/public/search/tabify/get_columns.ts @@ -22,10 +22,17 @@ import { IAggConfig } from '../aggs'; import { TabbedAggColumn } from './types'; const getColumn = (agg: IAggConfig, i: number): TabbedAggColumn => { + let name = ''; + try { + name = agg.makeLabel(); + } catch (e) { + // skip the case when makeLabel throws an error (e.x. no appropriate field for an aggregation) + } + return { aggConfig: agg, id: `col-${i}-${agg.id}`, - name: agg.makeLabel(), + name, }; }; diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx index 8e8054ac204d9..719827a98cc63 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx @@ -45,6 +45,7 @@ export class PhraseSuggestorUI extends React.Com PhraseSuggestorState > { private services = this.props.kibana.services; + private abortController?: AbortController; public state: PhraseSuggestorState = { suggestions: [], isLoading: false, @@ -54,6 +55,10 @@ export class PhraseSuggestorUI extends React.Com this.updateSuggestions(); } + public componentWillUnmount() { + if (this.abortController) this.abortController.abort(); + } + protected isSuggestingValues() { const shouldSuggestValues = this.services.uiSettings.get( UI_SETTINGS.FILTERS_EDITOR_SUGGEST_VALUES @@ -67,6 +72,8 @@ export class PhraseSuggestorUI extends React.Com }; protected updateSuggestions = debounce(async (query: string = '') => { + if (this.abortController) this.abortController.abort(); + this.abortController = new AbortController(); const { indexPattern, field } = this.props as PhraseSuggestorProps; if (!field || !this.isSuggestingValues()) { return; @@ -77,6 +84,7 @@ export class PhraseSuggestorUI extends React.Com indexPattern, field, query, + signal: this.abortController.signal, }); this.setState({ suggestions, isLoading: false }); diff --git a/src/plugins/data/public/ui/query_string_input/no_data_popover.test.tsx b/src/plugins/data/public/ui/query_string_input/no_data_popover.test.tsx new file mode 100644 index 0000000000000..27f924d98e6eb --- /dev/null +++ b/src/plugins/data/public/ui/query_string_input/no_data_popover.test.tsx @@ -0,0 +1,95 @@ +/* + * 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 from 'react'; +import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; +import { NoDataPopover } from './no_data_popover'; +import { EuiTourStep } from '@elastic/eui'; +import { act } from 'react-dom/test-utils'; + +describe('NoDataPopover', () => { + const createMockStorage = () => ({ + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), + }); + + it('should hide popover if showNoDataPopover is set to false', () => { + const Child = () => ; + const instance = mount( + + + + ); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + expect(instance.find(EuiTourStep).find(Child)).toHaveLength(1); + }); + + it('should hide popover if showNoDataPopover is set to true, but local storage flag is set', () => { + const child = ; + const storage = createMockStorage(); + storage.get.mockReturnValue(true); + const instance = mount( + + {child} + + ); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + }); + + it('should render popover if showNoDataPopover is set to true and local storage flag is not set', () => { + const child = ; + const instance = mount( + + {child} + + ); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(true); + }); + + it('should hide popover if it is closed', async () => { + const props = { + children: , + showNoDataPopover: true, + storage: createMockStorage(), + }; + const instance = mount(); + act(() => { + instance.find(EuiTourStep).prop('closePopover')!(); + }); + instance.setProps({ ...props }); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + }); + + it('should set local storage flag and hide on closing with button', () => { + const props = { + children: , + showNoDataPopover: true, + storage: createMockStorage(), + }; + const instance = mount(); + act(() => { + instance.find(EuiTourStep).prop('footerAction')!.props.onClick(); + }); + instance.setProps({ ...props }); + expect(props.storage.set).toHaveBeenCalledWith(expect.any(String), true); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + }); +}); diff --git a/src/plugins/data/public/ui/query_string_input/no_data_popover.tsx b/src/plugins/data/public/ui/query_string_input/no_data_popover.tsx new file mode 100644 index 0000000000000..302477a5fff5e --- /dev/null +++ b/src/plugins/data/public/ui/query_string_input/no_data_popover.tsx @@ -0,0 +1,96 @@ +/* + * 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 { ReactElement, useEffect, useState } from 'react'; +import React from 'react'; +import { EuiButtonEmpty, EuiText, EuiTourStep } from '@elastic/eui'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { i18n } from '@kbn/i18n'; + +const NO_DATA_POPOVER_STORAGE_KEY = 'data.noDataPopover'; + +export function NoDataPopover({ + showNoDataPopover, + storage, + children, +}: { + showNoDataPopover?: boolean; + storage: IStorageWrapper; + children: ReactElement; +}) { + const [noDataPopoverDismissed, setNoDataPopoverDismissed] = useState(() => + Boolean(storage.get(NO_DATA_POPOVER_STORAGE_KEY)) + ); + const [noDataPopoverVisible, setNoDataPopoverVisible] = useState(false); + + useEffect(() => { + if (showNoDataPopover && !noDataPopoverDismissed) { + setNoDataPopoverVisible(true); + } + }, [noDataPopoverDismissed, showNoDataPopover]); + + return ( + {}} + closePopover={() => { + setNoDataPopoverVisible(false); + }} + content={ + +

+ {i18n.translate('data.noDataPopover.content', { + defaultMessage: + "This time range doesn't contain any data. Increase or adjust the time range to see more fields and create charts", + })} +

+
+ } + minWidth={300} + anchorPosition="downCenter" + step={1} + stepsTotal={1} + isStepOpen={noDataPopoverVisible} + subtitle={i18n.translate('data.noDataPopover.title', { defaultMessage: 'Tip' })} + title="" + footerAction={ + { + storage.set(NO_DATA_POPOVER_STORAGE_KEY, true); + setNoDataPopoverDismissed(true); + setNoDataPopoverVisible(false); + }} + > + {i18n.translate('data.noDataPopover.dismissAction', { + defaultMessage: "Don't show again", + })} + + } + > +
{ + setNoDataPopoverVisible(false); + }} + > + {children} +
+
+ ); +} diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index f65bf97e391e2..4b0dc579c39ce 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -40,6 +40,7 @@ import { useKibana, toMountPoint } from '../../../../kibana_react/public'; import { QueryStringInput } from './query_string_input'; import { doesKueryExpressionHaveLuceneSyntaxError, UI_SETTINGS } from '../../../common'; import { PersistedLog, getQueryLog } from '../../query'; +import { NoDataPopover } from './no_data_popover'; interface Props { query?: Query; @@ -63,6 +64,7 @@ interface Props { customSubmitButton?: any; isDirty: boolean; timeHistory?: TimeHistoryContract; + indicateNoData?: boolean; } export function QueryBarTopRow(props: Props) { @@ -230,10 +232,12 @@ export function QueryBarTopRow(props: Props) { } return ( - - {renderDatePicker()} - {button} - + + + {renderDatePicker()} + {button} + + ); } diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 32295745ce217..120bbf3b68f7b 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -95,7 +95,7 @@ export class QueryStringInputUI extends Component { public inputRef: HTMLInputElement | null = null; private persistedLog: PersistedLog | undefined; - private abortController: AbortController | undefined; + private abortController?: AbortController; private services = this.props.kibana.services; private componentIsUnmounting = false; @@ -497,6 +497,7 @@ export class QueryStringInputUI extends Component { } public componentWillUnmount() { + if (this.abortController) this.abortController.abort(); this.updateSuggestions.cancel(); this.componentIsUnmounting = true; } diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 81e84e3198072..a0df7604f23aa 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -198,6 +198,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) showSaveQuery={props.showSaveQuery} screenTitle={props.screenTitle} indexPatterns={props.indexPatterns} + indicateNoData={props.indicateNoData} timeHistory={data.query.timefilter.history} dateRangeFrom={timeRange.from} dateRangeTo={timeRange.to} diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index a5ac227559115..2f740cc476087 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -75,6 +75,7 @@ export interface SearchBarOwnProps { onClearSavedQuery?: () => void; onRefresh?: (payload: { dateRange: TimeRange }) => void; + indicateNoData?: boolean; } export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; @@ -402,6 +403,7 @@ class SearchBarUI extends Component { this.props.customSubmitButton ? this.props.customSubmitButton : undefined } dataTestSubj={this.props.dataTestSubj} + indicateNoData={this.props.indicateNoData} /> ); } diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index 1a9d6bf4848f4..788ec1f145e2a 100644 --- a/src/plugins/dev_tools/public/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -16,21 +16,21 @@ * specific language governing permissions and limitations * under the License. */ + +import React, { useEffect, useRef } from 'react'; +import ReactDOM from 'react-dom'; +import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom'; +import { EuiTab, EuiTabs, EuiToolTip } from '@elastic/eui'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiTab, EuiTabs, EuiToolTip } from '@elastic/eui'; -import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom'; -import * as React from 'react'; -import ReactDOM from 'react-dom'; -import { useEffect, useRef } from 'react'; -import { AppMountContext, AppMountDeprecated, ScopedHistory } from 'kibana/public'; +import { ApplicationStart, ChromeStart, ScopedHistory } from 'src/core/public'; + import { DevToolApp } from './dev_tool'; interface DevToolsWrapperProps { devTools: readonly DevToolApp[]; activeDevTool: DevToolApp; - appMountContext: AppMountContext; updateRoute: (newRoute: string) => void; } @@ -40,12 +40,7 @@ interface MountedDevToolDescriptor { unmountHandler: () => void; } -function DevToolsWrapper({ - devTools, - activeDevTool, - appMountContext, - updateRoute, -}: DevToolsWrapperProps) { +function DevToolsWrapper({ devTools, activeDevTool, updateRoute }: DevToolsWrapperProps) { const mountedTool = useRef(null); useEffect( @@ -90,6 +85,7 @@ function DevToolsWrapper({ if (mountedTool.current) { mountedTool.current.unmountHandler(); } + const params = { element, appBasePath: '', @@ -97,9 +93,9 @@ function DevToolsWrapper({ // TODO: adapt to use Core's ScopedHistory history: {} as any, }; - const unmountHandler = isAppMountDeprecated(activeDevTool.mount) - ? await activeDevTool.mount(appMountContext, params) - : await activeDevTool.mount(params); + + const unmountHandler = await activeDevTool.mount(params); + mountedTool.current = { devTool: activeDevTool, mountpoint: element, @@ -112,19 +108,20 @@ function DevToolsWrapper({ ); } -function redirectOnMissingCapabilities(appMountContext: AppMountContext) { - if (!appMountContext.core.application.capabilities.dev_tools.show) { - appMountContext.core.application.navigateToApp('home'); +function redirectOnMissingCapabilities(application: ApplicationStart) { + if (!application.capabilities.dev_tools.show) { + application.navigateToApp('home'); return true; } return false; } -function setBadge(appMountContext: AppMountContext) { - if (appMountContext.core.application.capabilities.dev_tools.save) { +function setBadge(application: ApplicationStart, chrome: ChromeStart) { + if (application.capabilities.dev_tools.save) { return; } - appMountContext.core.chrome.setBadge({ + + chrome.setBadge({ text: i18n.translate('devTools.badge.readOnly.text', { defaultMessage: 'Read only', }), @@ -135,16 +132,16 @@ function setBadge(appMountContext: AppMountContext) { }); } -function setTitle(appMountContext: AppMountContext) { - appMountContext.core.chrome.docTitle.change( +function setTitle(chrome: ChromeStart) { + chrome.docTitle.change( i18n.translate('devTools.pageTitle', { defaultMessage: 'Dev Tools', }) ); } -function setBreadcrumbs(appMountContext: AppMountContext) { - appMountContext.core.chrome.setBreadcrumbs([ +function setBreadcrumbs(chrome: ChromeStart) { + chrome.setBreadcrumbs([ { text: i18n.translate('devTools.k7BreadcrumbsDevToolsLabel', { defaultMessage: 'Dev Tools', @@ -156,16 +153,19 @@ function setBreadcrumbs(appMountContext: AppMountContext) { export function renderApp( element: HTMLElement, - appMountContext: AppMountContext, + application: ApplicationStart, + chrome: ChromeStart, history: ScopedHistory, devTools: readonly DevToolApp[] ) { - if (redirectOnMissingCapabilities(appMountContext)) { + if (redirectOnMissingCapabilities(application)) { return () => {}; } - setBadge(appMountContext); - setBreadcrumbs(appMountContext); - setTitle(appMountContext); + + setBadge(application, chrome); + setBreadcrumbs(chrome); + setTitle(chrome); + ReactDOM.render( @@ -183,7 +183,6 @@ export function renderApp( updateRoute={props.history.push} activeDevTool={devTool} devTools={devTools} - appMountContext={appMountContext} /> )} /> @@ -208,8 +207,3 @@ export function renderApp( unlisten(); }; } - -function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated { - // Mount functions with two arguments are assumed to expect deprecated `context` object. - return mount.length === 2; -} diff --git a/src/plugins/dev_tools/public/dev_tool.ts b/src/plugins/dev_tools/public/dev_tool.ts index 943cca286a722..932897cdd7861 100644 --- a/src/plugins/dev_tools/public/dev_tool.ts +++ b/src/plugins/dev_tools/public/dev_tool.ts @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { App } from 'kibana/public'; + +import { AppMount } from 'src/core/public'; /** * Descriptor for a dev tool. A dev tool works similar to an application @@ -38,7 +39,7 @@ export class DevToolApp { * This will be used as a label in the tab above the actual tool. */ public readonly title: string; - public readonly mount: App['mount']; + public readonly mount: AppMount; /** * Flag indicating to disable the tab of this dev tool. Navigating to a @@ -66,7 +67,7 @@ export class DevToolApp { constructor( id: string, title: string, - mount: App['mount'], + mount: AppMount, enableRouting: boolean, order: number, toolTipContent = '', diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index 130d07b441b83..3ee44aaa0816e 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -18,12 +18,14 @@ */ import { BehaviorSubject } from 'rxjs'; -import { AppUpdater, CoreSetup, Plugin } from 'kibana/public'; +import { Plugin, CoreSetup, AppMountParameters } from 'src/core/public'; +import { AppUpdater } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { sortBy } from 'lodash'; + +import { AppNavLinkStatus, DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { KibanaLegacySetup } from '../../kibana_legacy/public'; import { CreateDevToolArgs, DevToolApp, createDevToolApp } from './dev_tool'; -import { AppNavLinkStatus, DEFAULT_APP_CATEGORIES } from '../../../core/public'; import './index.scss'; @@ -49,8 +51,10 @@ export class DevToolsPlugin implements Plugin { return sortBy([...this.devTools.values()], 'order'); } - public setup(core: CoreSetup, { kibanaLegacy }: { kibanaLegacy: KibanaLegacySetup }) { - core.application.register({ + public setup(coreSetup: CoreSetup, { kibanaLegacy }: { kibanaLegacy: KibanaLegacySetup }) { + const { application: applicationSetup, getStartServices } = coreSetup; + + applicationSetup.register({ id: 'dev_tools', title: i18n.translate('devTools.devToolsTitle', { defaultMessage: 'Dev Tools', @@ -59,15 +63,18 @@ export class DevToolsPlugin implements Plugin { euiIconType: 'devToolsApp', order: 9001, category: DEFAULT_APP_CATEGORIES.management, - mount: async (appMountContext, params) => { - if (!this.getSortedDevTools) { - throw new Error('not started yet'); - } + mount: async (params: AppMountParameters) => { + const { element, history } = params; + element.classList.add('devAppWrapper'); + + const [core] = await getStartServices(); + const { application, chrome } = core; + const { renderApp } = await import('./application'); - params.element.classList.add('devAppWrapper'); - return renderApp(params.element, appMountContext, params.history, this.getSortedDevTools()); + return renderApp(element, application, chrome, history, this.getSortedDevTools()); }, }); + kibanaLegacy.forwardApp('dev_tools', 'dev_tools'); return { diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover/public/application/angular/context_app.html index 9c37fd3bfc5be..6adcaeeae94f5 100644 --- a/src/plugins/discover/public/application/angular/context_app.html +++ b/src/plugins/discover/public/application/angular/context_app.html @@ -12,44 +12,10 @@ -
-
-
- - -
- -
-
-
-
- -
-
-
-
+ +
subArr[0] !== columnName) + : []; + setAppState({ columns, sort }); }; $scope.moveColumn = function moveColumn(columnName, newIndex) { diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx b/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx new file mode 100644 index 0000000000000..1c9439bc34e58 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx @@ -0,0 +1,54 @@ +/* + * 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 from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ReactWrapper } from 'enzyme'; +import { ContextErrorMessage } from './context_error_message'; +// @ts-ignore +import { FAILURE_REASONS, LOADING_STATUS } from '../../angular/context/query'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; + +describe('loading spinner', function () { + let component: ReactWrapper; + + it('ContextErrorMessage does not render on loading', () => { + component = mountWithIntl(); + expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(0); + }); + + it('ContextErrorMessage does not render on success loading', () => { + component = mountWithIntl(); + expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(0); + }); + + it('ContextErrorMessage renders just the title if the reason is not specifically handled', () => { + component = mountWithIntl(); + expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(1); + expect(findTestSubject(component, 'contextErrorMessageBody').text()).toBe(''); + }); + + it('ContextErrorMessage renders the reason for unknown errors', () => { + component = mountWithIntl( + + ); + expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(1); + expect(findTestSubject(component, 'contextErrorMessageBody').length).toBe(1); + }); +}); diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx b/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx new file mode 100644 index 0000000000000..f73496c2eeada --- /dev/null +++ b/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx @@ -0,0 +1,64 @@ +/* + * 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 from 'react'; +import { EuiCallOut, EuiText } from '@elastic/eui'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +// @ts-ignore +import { FAILURE_REASONS, LOADING_STATUS } from '../../angular/context/query'; + +export interface ContextErrorMessageProps { + /** + * the status of the loading action + */ + status: string; + /** + * the reason of the error + */ + reason?: string; +} + +export function ContextErrorMessage({ status, reason }: ContextErrorMessageProps) { + if (status !== LOADING_STATUS.FAILED) { + return null; + } + return ( + + + } + color="danger" + iconType="alert" + data-test-subj="contextErrorMessageTitle" + > + + {reason === FAILURE_REASONS.UNKNOWN && ( + + )} + + + + ); +} diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message_directive.ts b/src/plugins/discover/public/application/components/context_error_message/context_error_message_directive.ts new file mode 100644 index 0000000000000..925d560761a84 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_error_message/context_error_message_directive.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 { ContextErrorMessage } from './context_error_message'; + +export function createContextErrorMessageDirective(reactDirective: any) { + return reactDirective(ContextErrorMessage, [ + ['status', { watchDepth: 'reference' }], + ['reason', { watchDepth: 'reference' }], + ]); +} diff --git a/src/plugins/discover/public/application/components/context_error_message/index.ts b/src/plugins/discover/public/application/components/context_error_message/index.ts new file mode 100644 index 0000000000000..f20f2ccf8afa0 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_error_message/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { ContextErrorMessage } from './context_error_message'; +export { createContextErrorMessageDirective } from './context_error_message_directive'; diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index 05513eef93624..0b3c2fad8d45b 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -61,6 +61,7 @@ import { createDiscoverSidebarDirective } from './application/components/sidebar import { createHitsCounterDirective } from '././application/components/hits_counter'; import { createLoadingSpinnerDirective } from '././application/components/loading_spinner/loading_spinner'; import { createTimechartHeaderDirective } from './application/components/timechart_header'; +import { createContextErrorMessageDirective } from './application/components/context_error_message'; import { DiscoverStartPlugins } from './plugin'; import { getScopedHistory } from './kibana_services'; import { createSkipBottomButtonDirective } from './application/components/skip_bottom_button'; @@ -160,6 +161,7 @@ export function initializeInnerAngularModule( .directive('hitsCounter', createHitsCounterDirective) .directive('loadingSpinner', createLoadingSpinnerDirective) .directive('timechartHeader', createTimechartHeaderDirective) + .directive('contextErrorMessage', createContextErrorMessageDirective) .service('debounce', ['$timeout', DebounceProviderTimeout]); } diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index f19974942c43d..6960550b59d1c 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -48,8 +48,8 @@ export { EmbeddableOutput, EmbeddablePanel, EmbeddableRoot, - ValueClickTriggerContext, - RangeSelectTriggerContext, + ValueClickContext, + RangeSelectContext, ErrorEmbeddable, IContainer, IEmbeddable, diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index 5bb96a708b7ac..ccba5cf771088 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -25,7 +25,7 @@ export interface EmbeddableContext { embeddable: IEmbeddable; } -export interface ValueClickTriggerContext { +export interface ValueClickContext { embeddable?: T; data: { data: Array<{ @@ -39,7 +39,7 @@ export interface ValueClickTriggerContext { }; } -export interface RangeSelectTriggerContext { +export interface RangeSelectContext { embeddable?: T; data: { table: KibanaDatatable; @@ -50,16 +50,16 @@ export interface RangeSelectTriggerContext } export type ChartActionContext = - | ValueClickTriggerContext - | RangeSelectTriggerContext; + | ValueClickContext + | RangeSelectContext; export const isValueClickTriggerContext = ( context: ChartActionContext -): context is ValueClickTriggerContext => context.data && 'data' in context.data; +): context is ValueClickContext => context.data && 'data' in context.data; export const isRangeSelectTriggerContext = ( context: ChartActionContext -): context is RangeSelectTriggerContext => context.data && 'range' in context.data; +): context is RangeSelectContext => context.data && 'range' in context.data; export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = { diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts index a0de79da565e6..8d6a2d110efe0 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts @@ -35,10 +35,12 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe hidden: false, namespaceType: 'agnostic', mappings: { + dynamic: false, properties: { - appId: { type: 'keyword' }, - numberOfClicks: { type: 'long' }, - minutesOnScreen: { type: 'float' }, + // Disabled the mapping of these fields since they are not searched and we need to reduce the amount of indexed fields (#43673) + // appId: { type: 'keyword' }, + // numberOfClicks: { type: 'long' }, + // minutesOnScreen: { type: 'float' }, }, }, }); @@ -48,11 +50,13 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe hidden: false, namespaceType: 'agnostic', mappings: { + dynamic: false, properties: { timestamp: { type: 'date' }, - appId: { type: 'keyword' }, - numberOfClicks: { type: 'long' }, - minutesOnScreen: { type: 'float' }, + // Disabled the mapping of these fields since they are not searched and we need to reduce the amount of indexed fields (#43673) + // appId: { type: 'keyword' }, + // numberOfClicks: { type: 'long' }, + // minutesOnScreen: { type: 'float' }, }, }, }); diff --git a/src/plugins/maps_legacy/public/index.ts b/src/plugins/maps_legacy/public/index.ts index cbe8b9213d577..6b9c7d1c52db9 100644 --- a/src/plugins/maps_legacy/public/index.ts +++ b/src/plugins/maps_legacy/public/index.ts @@ -18,12 +18,10 @@ */ // @ts-ignore -import { CoreSetup, PluginInitializerContext } from 'kibana/public'; +import { PluginInitializerContext } from 'kibana/public'; // @ts-ignore import { L } from './leaflet'; -// @ts-ignore -import { KibanaMap } from './map/kibana_map'; -import { bindSetupCoreAndPlugins, MapsLegacyPlugin } from './plugin'; +import { MapsLegacyPlugin } from './plugin'; // @ts-ignore import * as colorUtil from './map/color_util'; // @ts-ignore @@ -32,8 +30,6 @@ import { KibanaMapLayer } from './map/kibana_map_layer'; import { convertToGeoJson } from './map/convert_to_geojson'; // @ts-ignore import { scaleBounds, getPrecision, geoContains } from './map/decode_geo_hash'; -// @ts-ignore -import { BaseMapsVisualizationProvider } from './map/base_maps_visualization'; import { VectorLayer, FileLayerField, @@ -75,20 +71,6 @@ export { L, }; -// Due to a leaflet/leaflet-draw bug, it's not possible to consume leaflet maps w/ draw control -// through a pipeline leveraging angular. For this reason, client plugins need to -// init kibana map and the basemaps visualization directly rather than consume through -// the usual plugin interface -export function getKibanaMapFactoryProvider(core: CoreSetup) { - bindSetupCoreAndPlugins(core); - return (...args: any) => new KibanaMap(...args); -} - -export function getBaseMapsVis(core: CoreSetup, serviceSettings: IServiceSettings) { - const getKibanaMap = getKibanaMapFactoryProvider(core); - return new BaseMapsVisualizationProvider(getKibanaMap, serviceSettings); -} - export * from './common/types'; export { ORIGIN } from './common/constants/origin'; diff --git a/src/plugins/maps_legacy/public/kibana_services.js b/src/plugins/maps_legacy/public/kibana_services.js index e0a6a6e21ab00..256b5f386d5f7 100644 --- a/src/plugins/maps_legacy/public/kibana_services.js +++ b/src/plugins/maps_legacy/public/kibana_services.js @@ -25,6 +25,12 @@ let uiSettings; export const setUiSettings = (coreUiSettings) => (uiSettings = coreUiSettings); export const getUiSettings = () => uiSettings; -let getInjectedVar; -export const setInjectedVarFunc = (getInjectedVarFunc) => (getInjectedVar = getInjectedVarFunc); -export const getInjectedVarFunc = () => getInjectedVar; +let kibanaVersion; +export const setKibanaVersion = (version) => (kibanaVersion = version); +export const getKibanaVersion = () => kibanaVersion; + +let mapsLegacyConfig; +export const setMapsLegacyConfig = (config) => (mapsLegacyConfig = config); +export const getMapsLegacyConfig = () => mapsLegacyConfig; + +export const getEmsTileLayerId = () => getMapsLegacyConfig().emsTileLayerId; diff --git a/src/plugins/maps_legacy/public/map/base_maps_visualization.js b/src/plugins/maps_legacy/public/map/base_maps_visualization.js index 2d1a45beb5d87..2d78fdc246e19 100644 --- a/src/plugins/maps_legacy/public/map/base_maps_visualization.js +++ b/src/plugins/maps_legacy/public/map/base_maps_visualization.js @@ -21,7 +21,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; import { filter, first } from 'rxjs/operators'; -import { getInjectedVarFunc, getUiSettings, getToasts } from '../kibana_services'; +import { getEmsTileLayerId, getUiSettings, getToasts } from '../kibana_services'; const WMS_MINZOOM = 0; const WMS_MAXZOOM = 22; //increase this to 22. Better for WMS @@ -129,7 +129,7 @@ export function BaseMapsVisualizationProvider(getKibanaMap, mapServiceSettings) } async _updateBaseLayer() { - const emsTileLayerId = getInjectedVarFunc()('emsTileLayerId', true); + const emsTileLayerId = getEmsTileLayerId(); if (!this._kibanaMap) { return; diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index 7c2b841e4adf3..f4f88bd5807d5 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -21,22 +21,20 @@ import _ from 'lodash'; import MarkdownIt from 'markdown-it'; import { EMSClient } from '@elastic/ems-client'; import { i18n } from '@kbn/i18n'; -import { getInjectedVarFunc } from '../kibana_services'; +import { getKibanaVersion } from '../kibana_services'; import { ORIGIN } from '../common/constants/origin'; const TMS_IN_YML_ID = 'TMS in config/kibana.yml'; export class ServiceSettings { constructor(mapConfig, tilemapsConfig) { - const getInjectedVar = getInjectedVarFunc(); this._mapConfig = mapConfig; this._tilemapsConfig = tilemapsConfig; - const kbnVersion = getInjectedVar('version'); this._showZoomMessage = true; this._emsClient = new EMSClient({ language: i18n.getLocale(), - appVersion: kbnVersion, + appVersion: getKibanaVersion(), appName: 'kibana', fileApiUrl: this._mapConfig.emsFileApiUrl, tileApiUrl: this._mapConfig.emsTileApiUrl, diff --git a/src/plugins/maps_legacy/public/plugin.ts b/src/plugins/maps_legacy/public/plugin.ts index 78c2498b9ee90..6b4e06fec9ccc 100644 --- a/src/plugins/maps_legacy/public/plugin.ts +++ b/src/plugins/maps_legacy/public/plugin.ts @@ -20,13 +20,17 @@ // @ts-ignore import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; // @ts-ignore -import { setToasts, setUiSettings, setInjectedVarFunc } from './kibana_services'; +import { setToasts, setUiSettings, setKibanaVersion, setMapsLegacyConfig } from './kibana_services'; // @ts-ignore import { ServiceSettings } from './map/service_settings'; // @ts-ignore import { getPrecision, getZoomPrecision } from './map/precision'; +// @ts-ignore +import { KibanaMap } from './map/kibana_map'; import { MapsLegacyConfigType, MapsLegacyPluginSetup, MapsLegacyPluginStart } from './index'; import { ConfigSchema } from '../config'; +// @ts-ignore +import { BaseMapsVisualizationProvider } from './map/base_maps_visualization'; /** * These are the interfaces with your public contracts. You should export these @@ -34,10 +38,15 @@ import { ConfigSchema } from '../config'; * @public */ -export const bindSetupCoreAndPlugins = (core: CoreSetup) => { +export const bindSetupCoreAndPlugins = ( + core: CoreSetup, + config: MapsLegacyConfigType, + kibanaVersion: string +) => { setToasts(core.notifications.toasts); setUiSettings(core.uiSettings); - setInjectedVarFunc(core.injectedMetadata.getInjectedVar); + setKibanaVersion(kibanaVersion); + setMapsLegacyConfig(config); }; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -53,15 +62,23 @@ export class MapsLegacyPlugin implements Plugin(); + const kibanaVersion = this._initializerContext.env.packageInfo.version; + + bindSetupCoreAndPlugins(core, config, kibanaVersion); + + const serviceSettings = new ServiceSettings(config, config.tilemap); + const getKibanaMapFactoryProvider = (...args: any) => new KibanaMap(...args); + const getBaseMapsVis = () => + new BaseMapsVisualizationProvider(getKibanaMapFactoryProvider, serviceSettings); return { - serviceSettings: new ServiceSettings(config, config.tilemap), + serviceSettings, getZoomPrecision, getPrecision, config, + getKibanaMapFactoryProvider, + getBaseMapsVis, }; } diff --git a/src/plugins/region_map/public/__tests__/region_map_visualization.js b/src/plugins/region_map/public/__tests__/region_map_visualization.js index 3dcfc7c2fc6fa..0a2a18c7cef4f 100644 --- a/src/plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/plugins/region_map/public/__tests__/region_map_visualization.js @@ -52,10 +52,11 @@ import { ExprVis } from '../../../visualizations/public/expressions/vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { BaseVisType } from '../../../visualizations/public/vis_types/base_vis_type'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setInjectedVarFunc } from '../../../maps_legacy/public/kibana_services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceSettings } from '../../../maps_legacy/public/map/service_settings'; -import { getBaseMapsVis } from '../../../maps_legacy/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { BaseMapsVisualizationProvider } from '../../../maps_legacy/public/map/base_maps_visualization'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaMap } from '../../../maps_legacy/public/map/kibana_map'; const THRESHOLD = 0.45; const PIXEL_DIFF = 96; @@ -118,14 +119,6 @@ describe('RegionMapsVisualizationTests', function () { }, }, }; - setInjectedVarFunc((injectedVar) => { - switch (injectedVar) { - case 'version': - return '123'; - default: - return 'not found'; - } - }); const serviceSettings = new ServiceSettings(mapConfig, tilemapsConfig); const regionmapsConfig = { includeElasticMapsService: true, @@ -142,7 +135,10 @@ describe('RegionMapsVisualizationTests', function () { getInjectedVar: () => {}, }, }; - const BaseMapsVisualization = getBaseMapsVis(coreSetupMock, serviceSettings); + const BaseMapsVisualization = new BaseMapsVisualizationProvider( + (...args) => new KibanaMap(...args), + serviceSettings + ); dependencies = { serviceSettings, diff --git a/src/plugins/region_map/public/plugin.ts b/src/plugins/region_map/public/plugin.ts index 6b31de758a4ca..04a2ba2f23f4e 100644 --- a/src/plugins/region_map/public/plugin.ts +++ b/src/plugins/region_map/public/plugin.ts @@ -30,7 +30,7 @@ import { VisualizationsSetup } from '../../visualizations/public'; import { createRegionMapFn } from './region_map_fn'; // @ts-ignore import { createRegionMapTypeDefinition } from './region_map_type'; -import { getBaseMapsVis, IServiceSettings, MapsLegacyPluginSetup } from '../../maps_legacy/public'; +import { IServiceSettings, MapsLegacyPluginSetup } from '../../maps_legacy/public'; import { setFormatService, setNotifications, setKibanaLegacy } from './kibana_services'; import { DataPublicPluginStart } from '../../data/public'; import { RegionMapsConfigType } from './index'; @@ -94,7 +94,7 @@ export class RegionMapPlugin implements Plugin { describe('handleLocalStats', () => { it('returns expected object without xpack and kibana data', () => { - const result = handleLocalStats(clusterInfo, clusterStatsWithNodesUsage, void 0, context); + const result = handleLocalStats( + clusterInfo, + clusterStatsWithNodesUsage, + void 0, + void 0, + context + ); expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid); expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name); expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats); expect(result.version).to.be('2.3.4'); expect(result.collection).to.be('local'); expect(result.license).to.be(undefined); - expect(result.stack_stats).to.eql({ kibana: undefined }); + expect(result.stack_stats).to.eql({ kibana: undefined, data: undefined }); }); it('returns expected object with xpack', () => { - const result = handleLocalStats(clusterInfo, clusterStatsWithNodesUsage, void 0, context); + const result = handleLocalStats( + clusterInfo, + clusterStatsWithNodesUsage, + void 0, + void 0, + context + ); const { stack_stats: stack, ...cluster } = result; expect(cluster.collection).to.be(combinedStatsResult.collection); expect(cluster.cluster_uuid).to.be(combinedStatsResult.cluster_uuid); expect(cluster.cluster_name).to.be(combinedStatsResult.cluster_name); expect(stack.kibana).to.be(undefined); // not mocked for this test + expect(stack.data).to.be(undefined); // not mocked for this test expect(cluster.version).to.eql(combinedStatsResult.version); expect(cluster.cluster_stats).to.eql(combinedStatsResult.cluster_stats); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts new file mode 100644 index 0000000000000..2d0864b1cb75f --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts @@ -0,0 +1,136 @@ +/* + * 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 const DATA_TELEMETRY_ID = 'data'; + +export const DATA_KNOWN_TYPES = ['logs', 'traces', 'metrics'] as const; + +export type DataTelemetryType = typeof DATA_KNOWN_TYPES[number]; + +export type DataPatternName = typeof DATA_DATASETS_INDEX_PATTERNS[number]['patternName']; + +// TODO: Ideally this list should be updated from an external public URL (similar to the newsfeed) +// But it's good to have a minimum list shipped with the build. +export const DATA_DATASETS_INDEX_PATTERNS = [ + // Enterprise Search - Elastic + { pattern: '.ent-search-*', patternName: 'enterprise-search' }, + { pattern: '.app-search-*', patternName: 'app-search' }, + // Enterprise Search - 3rd party + { pattern: '*magento2*', patternName: 'magento2' }, + { pattern: '*magento*', patternName: 'magento' }, + { pattern: '*shopify*', patternName: 'shopify' }, + { pattern: '*wordpress*', patternName: 'wordpress' }, + // { pattern: '*wp*', patternName: 'wordpress' }, // TODO: Too vague? + { pattern: '*drupal*', patternName: 'drupal' }, + { pattern: '*joomla*', patternName: 'joomla' }, + { pattern: '*search*', patternName: 'search' }, // TODO: Too vague? + // { pattern: '*wix*', patternName: 'wix' }, // TODO: Too vague? + { pattern: '*sharepoint*', patternName: 'sharepoint' }, + { pattern: '*squarespace*', patternName: 'squarespace' }, + // { pattern: '*aem*', patternName: 'aem' }, // TODO: Too vague? + { pattern: '*sitecore*', patternName: 'sitecore' }, + { pattern: '*weebly*', patternName: 'weebly' }, + { pattern: '*acquia*', patternName: 'acquia' }, + + // Observability - Elastic + { pattern: 'filebeat-*', patternName: 'filebeat', shipper: 'filebeat' }, + { pattern: 'metricbeat-*', patternName: 'metricbeat', shipper: 'metricbeat' }, + { pattern: 'apm-*', patternName: 'apm', shipper: 'apm' }, + { pattern: 'functionbeat-*', patternName: 'functionbeat', shipper: 'functionbeat' }, + { pattern: 'heartbeat-*', patternName: 'heartbeat', shipper: 'heartbeat' }, + { pattern: 'logstash-*', patternName: 'logstash', shipper: 'logstash' }, + // Observability - 3rd party + { pattern: 'fluentd*', patternName: 'fluentd' }, + { pattern: 'telegraf*', patternName: 'telegraf' }, + { pattern: 'prometheusbeat*', patternName: 'prometheusbeat' }, + { pattern: 'fluentbit*', patternName: 'fluentbit' }, + { pattern: '*nginx*', patternName: 'nginx' }, + { pattern: '*apache*', patternName: 'apache' }, // Already in Security (keeping it in here for documentation) + // { pattern: '*logs*', patternName: 'third-party-logs' }, Disabled for now + + // Security - Elastic + { pattern: 'logstash-*', patternName: 'logstash', shipper: 'logstash' }, + { pattern: 'endgame-*', patternName: 'endgame', shipper: 'endgame' }, + { pattern: 'logs-endpoint.*', patternName: 'logs-endpoint', shipper: 'endpoint' }, // It should be caught by the `mappings` logic, but just in case + { pattern: 'metrics-endpoint.*', patternName: 'metrics-endpoint', shipper: 'endpoint' }, // It should be caught by the `mappings` logic, but just in case + { pattern: '.siem-signals-*', patternName: 'siem-signals' }, + { pattern: 'auditbeat-*', patternName: 'auditbeat', shipper: 'auditbeat' }, + { pattern: 'winlogbeat-*', patternName: 'winlogbeat', shipper: 'winlogbeat' }, + { pattern: 'packetbeat-*', patternName: 'packetbeat', shipper: 'packetbeat' }, + { pattern: 'filebeat-*', patternName: 'filebeat', shipper: 'filebeat' }, + // Security - 3rd party + { pattern: '*apache*', patternName: 'apache' }, // Already in Observability (keeping it in here for documentation) + { pattern: '*tomcat*', patternName: 'tomcat' }, + { pattern: '*artifactory*', patternName: 'artifactory' }, + { pattern: '*aruba*', patternName: 'aruba' }, + { pattern: '*barracuda*', patternName: 'barracuda' }, + { pattern: '*bluecoat*', patternName: 'bluecoat' }, + { pattern: 'arcsight-*', patternName: 'arcsight', shipper: 'arcsight' }, + // { pattern: '*cef*', patternName: 'cef' }, // Disabled because it's too vague + { pattern: '*checkpoint*', patternName: 'checkpoint' }, + { pattern: '*cisco*', patternName: 'cisco' }, + { pattern: '*citrix*', patternName: 'citrix' }, + { pattern: '*cyberark*', patternName: 'cyberark' }, + { pattern: '*cylance*', patternName: 'cylance' }, + { pattern: '*fireeye*', patternName: 'fireeye' }, + { pattern: '*fortinet*', patternName: 'fortinet' }, + { pattern: '*infoblox*', patternName: 'infoblox' }, + { pattern: '*kaspersky*', patternName: 'kaspersky' }, + { pattern: '*mcafee*', patternName: 'mcafee' }, + // paloaltonetworks + { pattern: '*paloaltonetworks*', patternName: 'paloaltonetworks' }, + { pattern: 'pan-*', patternName: 'paloaltonetworks' }, + { pattern: 'pan_*', patternName: 'paloaltonetworks' }, + { pattern: 'pan.*', patternName: 'paloaltonetworks' }, + + // rsa + { pattern: 'rsa.*', patternName: 'rsa' }, + { pattern: 'rsa-*', patternName: 'rsa' }, + { pattern: 'rsa_*', patternName: 'rsa' }, + + // snort + { pattern: 'snort-*', patternName: 'snort' }, + { pattern: 'logstash-snort*', patternName: 'snort' }, + + { pattern: '*sonicwall*', patternName: 'sonicwall' }, + { pattern: '*sophos*', patternName: 'sophos' }, + + // squid + { pattern: 'squid-*', patternName: 'squid' }, + { pattern: 'squid_*', patternName: 'squid' }, + { pattern: 'squid.*', patternName: 'squid' }, + + { pattern: '*symantec*', patternName: 'symantec' }, + { pattern: '*tippingpoint*', patternName: 'tippingpoint' }, + { pattern: '*trendmicro*', patternName: 'trendmicro' }, + { pattern: '*tripwire*', patternName: 'tripwire' }, + { pattern: '*zscaler*', patternName: 'zscaler' }, + { pattern: '*zeek*', patternName: 'zeek' }, + { pattern: '*sigma_doc*', patternName: 'sigma_doc' }, + // { pattern: '*bro*', patternName: 'bro' }, // Disabled because it's too vague + { pattern: 'ecs-corelight*', patternName: 'ecs-corelight' }, + { pattern: '*suricata*', patternName: 'suricata' }, + // { pattern: '*fsf*', patternName: 'fsf' }, // Disabled because it's too vague + { pattern: '*wazuh*', patternName: 'wazuh' }, +] as const; + +// Get the unique list of index patterns (some are duplicated for documentation purposes) +export const DATA_DATASETS_INDEX_PATTERNS_UNIQUE = DATA_DATASETS_INDEX_PATTERNS.filter( + (entry, index, array) => !array.slice(0, index).find(({ pattern }) => entry.pattern === pattern) +); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts new file mode 100644 index 0000000000000..8bffc5d012a74 --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts @@ -0,0 +1,251 @@ +/* + * 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 { buildDataTelemetryPayload, getDataTelemetry } from './get_data_telemetry'; +import { DATA_DATASETS_INDEX_PATTERNS, DATA_DATASETS_INDEX_PATTERNS_UNIQUE } from './constants'; + +describe('get_data_telemetry', () => { + describe('DATA_DATASETS_INDEX_PATTERNS', () => { + DATA_DATASETS_INDEX_PATTERNS.forEach((entry, index, array) => { + describe(`Pattern ${entry.pattern}`, () => { + test('there should only be one in DATA_DATASETS_INDEX_PATTERNS_UNIQUE', () => { + expect( + DATA_DATASETS_INDEX_PATTERNS_UNIQUE.filter(({ pattern }) => pattern === entry.pattern) + ).toHaveLength(1); + }); + + // This test is to make us sure that we don't update one of the duplicated entries and forget about any other repeated ones + test('when a document is duplicated, the duplicates should be identical', () => { + array.slice(0, index).forEach((previousEntry) => { + if (entry.pattern === previousEntry.pattern) { + expect(entry).toStrictEqual(previousEntry); + } + }); + }); + }); + }); + }); + + describe('buildDataTelemetryPayload', () => { + test('return the base object when no indices provided', () => { + expect(buildDataTelemetryPayload([])).toStrictEqual([]); + }); + + test('return the base object when no matching indices provided', () => { + expect( + buildDataTelemetryPayload([ + { name: 'no__way__this__can_match_anything', sizeInBytes: 10 }, + { name: '.kibana-event-log-8.0.0' }, + ]) + ).toStrictEqual([]); + }); + + test('matches some indices and puts them in their own category', () => { + expect( + buildDataTelemetryPayload([ + // APM Indices have known shipper (so we can infer the datasetType from mapping constant) + { name: 'apm-7.7.0-error-000001', shipper: 'apm', isECS: true }, + { name: 'apm-7.7.0-metric-000001', shipper: 'apm', isECS: true }, + { name: 'apm-7.7.0-onboarding-2020.05.17', shipper: 'apm', isECS: true }, + { name: 'apm-7.7.0-profile-000001', shipper: 'apm', isECS: true }, + { name: 'apm-7.7.0-span-000001', shipper: 'apm', isECS: true }, + { name: 'apm-7.7.0-transaction-000001', shipper: 'apm', isECS: true }, + // Packetbeat indices with known shipper (we can infer datasetType from mapping constant) + { name: 'packetbeat-7.7.0-2020.06.11-000001', shipper: 'packetbeat', isECS: true }, + // Matching patterns from the list => known datasetName but the rest is unknown + { name: 'filebeat-12314', docCount: 100, sizeInBytes: 10 }, + { name: 'metricbeat-1234', docCount: 100, sizeInBytes: 10, isECS: false }, + { name: '.app-search-1234', docCount: 0 }, + { name: 'logs-endpoint.1234', docCount: 0 }, // Matching pattern with a dot in the name + // New Indexing strategy: everything can be inferred from the constant_keyword values + { + name: 'logs-nginx.access-default-000001', + datasetName: 'nginx.access', + datasetType: 'logs', + shipper: 'filebeat', + isECS: true, + docCount: 1000, + sizeInBytes: 1000, + }, + { + name: 'logs-nginx.access-default-000002', + datasetName: 'nginx.access', + datasetType: 'logs', + shipper: 'filebeat', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, + ]) + ).toStrictEqual([ + { + shipper: 'apm', + index_count: 6, + ecs_index_count: 6, + }, + { + shipper: 'packetbeat', + index_count: 1, + ecs_index_count: 1, + }, + { + pattern_name: 'filebeat', + shipper: 'filebeat', + index_count: 1, + doc_count: 100, + size_in_bytes: 10, + }, + { + pattern_name: 'metricbeat', + shipper: 'metricbeat', + index_count: 1, + ecs_index_count: 0, + doc_count: 100, + size_in_bytes: 10, + }, + { + pattern_name: 'app-search', + index_count: 1, + doc_count: 0, + }, + { + pattern_name: 'logs-endpoint', + shipper: 'endpoint', + index_count: 1, + doc_count: 0, + }, + { + dataset: { name: 'nginx.access', type: 'logs' }, + shipper: 'filebeat', + index_count: 2, + ecs_index_count: 2, + doc_count: 2000, + size_in_bytes: 1060, + }, + ]); + }); + }); + + describe('getDataTelemetry', () => { + test('it returns the base payload (all 0s) because no indices are found', async () => { + const callCluster = mockCallCluster(); + await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([]); + }); + + test('can only see the index mappings, but not the stats', async () => { + const callCluster = mockCallCluster(['filebeat-12314']); + await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([ + { + pattern_name: 'filebeat', + shipper: 'filebeat', + index_count: 1, + ecs_index_count: 0, + }, + ]); + }); + + test('can see the mappings and the stats', async () => { + const callCluster = mockCallCluster( + ['filebeat-12314'], + { isECS: true }, + { + indices: { + 'filebeat-12314': { total: { docs: { count: 100 }, store: { size_in_bytes: 10 } } }, + }, + } + ); + await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([ + { + pattern_name: 'filebeat', + shipper: 'filebeat', + index_count: 1, + ecs_index_count: 1, + doc_count: 100, + size_in_bytes: 10, + }, + ]); + }); + + test('find an index that does not match any index pattern but has mappings metadata', async () => { + const callCluster = mockCallCluster( + ['cannot_match_anything'], + { isECS: true, datasetType: 'traces', shipper: 'my-beat' }, + { + indices: { + cannot_match_anything: { + total: { docs: { count: 100 }, store: { size_in_bytes: 10 } }, + }, + }, + } + ); + await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([ + { + dataset: { name: undefined, type: 'traces' }, + shipper: 'my-beat', + index_count: 1, + ecs_index_count: 1, + doc_count: 100, + size_in_bytes: 10, + }, + ]); + }); + + test('return empty array when there is an error', async () => { + const callCluster = jest.fn().mockRejectedValue(new Error('Something went terribly wrong')); + await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([]); + }); + }); +}); + +function mockCallCluster( + indicesMappings: string[] = [], + { isECS = false, datasetName = '', datasetType = '', shipper = '' } = {}, + indexStats: any = {} +) { + return jest.fn().mockImplementation(async (method: string, opts: any) => { + if (method === 'indices.getMapping') { + return Object.fromEntries( + indicesMappings.map((index) => [ + index, + { + mappings: { + ...(shipper && { _meta: { beat: shipper } }), + properties: { + ...(isECS && { ecs: { properties: { version: { type: 'keyword' } } } }), + ...((datasetType || datasetName) && { + dataset: { + properties: { + ...(datasetName && { + name: { type: 'constant_keyword', value: datasetName }, + }), + ...(datasetType && { + type: { type: 'constant_keyword', value: datasetType }, + }), + }, + }, + }), + }, + }, + }, + ]) + ); + } + return indexStats; + }); +} diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts new file mode 100644 index 0000000000000..cf906bc5c86cf --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts @@ -0,0 +1,253 @@ +/* + * 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 { LegacyAPICaller } from 'kibana/server'; +import { + DATA_DATASETS_INDEX_PATTERNS_UNIQUE, + DataPatternName, + DataTelemetryType, +} from './constants'; + +export interface DataTelemetryBasePayload { + index_count: number; + ecs_index_count?: number; + doc_count?: number; + size_in_bytes?: number; +} + +export interface DataTelemetryDocument extends DataTelemetryBasePayload { + dataset?: { + name?: string; + type?: DataTelemetryType | 'unknown' | string; // The union of types is to help autocompletion with some known `dataset.type`s + }; + shipper?: string; + pattern_name?: DataPatternName; +} + +export type DataTelemetryPayload = DataTelemetryDocument[]; + +export interface DataTelemetryIndex { + name: string; + datasetName?: string; // To be obtained from `mappings.dataset.name` if it's a constant keyword + datasetType?: string; // To be obtained from `mappings.dataset.type` if it's a constant keyword + shipper?: string; // To be obtained from `_meta.beat` if it's set + isECS?: boolean; // Optional because it can't be obtained via Monitoring. + + // The fields below are optional because we might not be able to obtain them if the user does not + // have access to the index. + docCount?: number; + sizeInBytes?: number; +} + +type AtLeastOne }> = Partial & U[keyof U]; + +type DataDescriptor = AtLeastOne<{ + datasetName: string; + datasetType: string; + shipper: string; + patternName: DataPatternName; // When found from the list of the index patterns +}>; + +function findMatchingDescriptors({ + name, + shipper, + datasetName, + datasetType, +}: DataTelemetryIndex): DataDescriptor[] { + // If we already have the data from the indices' mappings... + if ([shipper, datasetName, datasetType].some(Boolean)) { + return [ + { + ...(shipper && { shipper }), + ...(datasetName && { datasetName }), + ...(datasetType && { datasetType }), + } as AtLeastOne<{ datasetName: string; datasetType: string; shipper: string }>, // Using casting here because TS doesn't infer at least one exists from the if clause + ]; + } + + // Otherwise, try with the list of known index patterns + return DATA_DATASETS_INDEX_PATTERNS_UNIQUE.filter(({ pattern }) => { + if (!pattern.startsWith('.') && name.startsWith('.')) { + // avoid system indices caught by very fuzzy index patterns (i.e.: *log* would catch `.kibana-log-...`) + return false; + } + return new RegExp(`^${pattern.replace(/\./g, '\\.').replace(/\*/g, '.*')}$`).test(name); + }); +} + +function increaseCounters( + previousValue: DataTelemetryBasePayload = { index_count: 0 }, + { isECS, docCount, sizeInBytes }: DataTelemetryIndex +) { + return { + ...previousValue, + index_count: previousValue.index_count + 1, + ...(typeof isECS === 'boolean' + ? { + ecs_index_count: (previousValue.ecs_index_count || 0) + (isECS ? 1 : 0), + } + : {}), + ...(typeof docCount === 'number' + ? { doc_count: (previousValue.doc_count || 0) + docCount } + : {}), + ...(typeof sizeInBytes === 'number' + ? { size_in_bytes: (previousValue.size_in_bytes || 0) + sizeInBytes } + : {}), + }; +} + +export function buildDataTelemetryPayload(indices: DataTelemetryIndex[]): DataTelemetryPayload { + const startingDotPatternsUntilTheFirstAsterisk = DATA_DATASETS_INDEX_PATTERNS_UNIQUE.map( + ({ pattern }) => pattern.replace(/^\.(.+)\*.*$/g, '.$1') + ).filter(Boolean); + + // Filter out the system indices unless they are required by the patterns + const indexCandidates = indices.filter( + ({ name }) => + !( + name.startsWith('.') && + !startingDotPatternsUntilTheFirstAsterisk.find((pattern) => name.startsWith(pattern)) + ) + ); + + const acc = new Map(); + + for (const indexCandidate of indexCandidates) { + const matchingDescriptors = findMatchingDescriptors(indexCandidate); + for (const { datasetName, datasetType, shipper, patternName } of matchingDescriptors) { + const key = `${datasetName}-${datasetType}-${shipper}-${patternName}`; + acc.set(key, { + ...((datasetName || datasetType) && { dataset: { name: datasetName, type: datasetType } }), + ...(shipper && { shipper }), + ...(patternName && { pattern_name: patternName }), + ...increaseCounters(acc.get(key), indexCandidate), + }); + } + } + + return [...acc.values()]; +} + +interface IndexStats { + indices: { + [indexName: string]: { + total: { + docs: { + count: number; + deleted: number; + }; + store: { + size_in_bytes: number; + }; + }; + }; + }; +} + +interface IndexMappings { + [indexName: string]: { + mappings: { + _meta?: { + beat?: string; + }; + properties: { + dataset?: { + properties: { + name?: { + type: string; + value?: string; + }; + type?: { + type: string; + value?: string; + }; + }; + }; + ecs?: { + properties: { + version?: { + type: string; + }; + }; + }; + }; + }; + }; +} + +export async function getDataTelemetry(callCluster: LegacyAPICaller) { + try { + const index = [ + ...DATA_DATASETS_INDEX_PATTERNS_UNIQUE.map(({ pattern }) => pattern), + '*-*-*-*', // Include new indexing strategy indices {type}-{dataset}-{namespace}-{rollover_counter} + ]; + const [indexMappings, indexStats]: [IndexMappings, IndexStats] = await Promise.all([ + // GET */_mapping?filter_path=*.mappings._meta.beat,*.mappings.properties.ecs.properties.version.type,*.mappings.properties.dataset.properties.type.value,*.mappings.properties.dataset.properties.name.value + callCluster('indices.getMapping', { + index: '*', // Request all indices because filter_path already filters out the indices without any of those fields + filterPath: [ + // _meta.beat tells the shipper + '*.mappings._meta.beat', + // Does it have `ecs.version` in the mappings? => It follows the ECS conventions + '*.mappings.properties.ecs.properties.version.type', + + // Disable the fields below because they are still pending to be confirmed: + // https://github.com/elastic/ecs/pull/845 + // TODO: Re-enable when the final fields are confirmed + // // If `dataset.type` is a `constant_keyword`, it can be reported as a type + // '*.mappings.properties.dataset.properties.type.value', + // // If `dataset.name` is a `constant_keyword`, it can be reported as the dataset + // '*.mappings.properties.dataset.properties.name.value', + ], + }), + // GET /_stats/docs,store?level=indices&filter_path=indices.*.total + callCluster('indices.stats', { + index, + level: 'indices', + metric: ['docs', 'store'], + filterPath: ['indices.*.total'], + }), + ]); + + const indexNames = Object.keys({ ...indexMappings, ...indexStats?.indices }); + const indices = indexNames.map((name) => { + const isECS = !!indexMappings[name]?.mappings?.properties.ecs?.properties.version?.type; + const shipper = indexMappings[name]?.mappings?._meta?.beat; + const datasetName = indexMappings[name]?.mappings?.properties.dataset?.properties.name?.value; + const datasetType = indexMappings[name]?.mappings?.properties.dataset?.properties.type?.value; + + const stats = (indexStats?.indices || {})[name]; + if (stats) { + return { + name, + datasetName, + datasetType, + shipper, + isECS, + docCount: stats.total?.docs?.count, + sizeInBytes: stats.total?.store?.size_in_bytes, + }; + } + return { name, datasetName, datasetType, shipper, isECS }; + }); + return buildDataTelemetryPayload(indices); + } catch (e) { + return []; + } +} diff --git a/test/plugin_functional/plugins/core_logging/server/index.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts similarity index 78% rename from test/plugin_functional/plugins/core_logging/server/index.ts rename to src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts index ca1d9da95b495..d056d1c9f299f 100644 --- a/test/plugin_functional/plugins/core_logging/server/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts @@ -17,7 +17,11 @@ * under the License. */ -import type { PluginInitializerContext } from '../../../../../src/core/server'; -import { CoreLoggingPlugin } from './plugin'; +export { DATA_TELEMETRY_ID } from './constants'; -export const plugin = (init: PluginInitializerContext) => new CoreLoggingPlugin(init); +export { + DataTelemetryIndex, + DataTelemetryPayload, + getDataTelemetry, + buildDataTelemetryPayload, +} from './get_data_telemetry'; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index b42edde2f55ca..4d4031bb428ba 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -25,6 +25,7 @@ import { getClusterInfo, ESClusterInfo } from './get_cluster_info'; import { getClusterStats } from './get_cluster_stats'; import { getKibana, handleKibanaStats, KibanaUsageStats } from './get_kibana'; import { getNodesUsage } from './get_nodes_usage'; +import { getDataTelemetry, DATA_TELEMETRY_ID, DataTelemetryPayload } from './get_data_telemetry'; /** * Handle the separate local calls by combining them into a single object response that looks like the @@ -39,6 +40,7 @@ export function handleLocalStats( { cluster_name, cluster_uuid, version }: ESClusterInfo, { _nodes, cluster_name: clusterName, ...clusterStats }: any, kibana: KibanaUsageStats, + dataTelemetry: DataTelemetryPayload, context: StatsCollectionContext ) { return { @@ -49,6 +51,7 @@ export function handleLocalStats( cluster_stats: clusterStats, collection: 'local', stack_stats: { + [DATA_TELEMETRY_ID]: dataTelemetry, kibana: handleKibanaStats(context, kibana), }, }; @@ -68,11 +71,12 @@ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( return await Promise.all( clustersDetails.map(async (clustersDetail) => { - const [clusterInfo, clusterStats, nodesUsage, kibana] = await Promise.all([ + const [clusterInfo, clusterStats, nodesUsage, kibana, dataTelemetry] = await Promise.all([ getClusterInfo(callCluster), // cluster info getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_) getNodesUsage(callCluster), // nodes_usage info getKibana(usageCollection, callCluster), + getDataTelemetry(callCluster), ]); return handleLocalStats( clusterInfo, @@ -81,6 +85,7 @@ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( nodes: { ...clusterStats.nodes, usage: nodesUsage }, }, kibana, + dataTelemetry, context ); }) diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts index 377ddab7b877c..40cbf0e4caa1d 100644 --- a/src/plugins/telemetry/server/telemetry_collection/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/index.ts @@ -17,6 +17,12 @@ * under the License. */ +export { + DATA_TELEMETRY_ID, + DataTelemetryIndex, + DataTelemetryPayload, + buildDataTelemetryPayload, +} from './get_data_telemetry'; export { getLocalStats, TelemetryLocalStats } from './get_local_stats'; export { getLocalLicense } from './get_local_license'; export { getClusterUuids } from './get_cluster_stats'; diff --git a/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js index 11c8fb9c00ef1..9ff25ce674d3d 100644 --- a/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js +++ b/src/plugins/tile_map/public/__tests__/coordinate_maps_visualization.js @@ -52,8 +52,9 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceSettings } from '../../../maps_legacy/public/map/service_settings'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setInjectedVarFunc } from '../../../maps_legacy/public/kibana_services'; -import { getBaseMapsVis } from '../../../maps_legacy/public'; +import { KibanaMap } from '../../../maps_legacy/public/map/kibana_map'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { BaseMapsVisualizationProvider } from '../../../maps_legacy/public/map/base_maps_visualization'; function mockRawData() { const stack = [dummyESResponse]; @@ -105,26 +106,12 @@ describe('CoordinateMapsVisualizationTest', function () { }, }, }; - setInjectedVarFunc((injectedVar) => { - switch (injectedVar) { - case 'version': - return '123'; - default: - return 'not found'; - } - }); - const coreSetupMock = { - notifications: { - toasts: {}, - }, - uiSettings: {}, - injectedMetadata: { - getInjectedVar: () => {}, - }, - }; const serviceSettings = new ServiceSettings(mapConfig, tilemapsConfig); - const BaseMapsVisualization = getBaseMapsVis(coreSetupMock, serviceSettings); + const BaseMapsVisualization = new BaseMapsVisualizationProvider( + (...args) => new KibanaMap(...args), + serviceSettings + ); const uiSettings = $injector.get('config'); dependencies = { diff --git a/src/plugins/tile_map/public/plugin.ts b/src/plugins/tile_map/public/plugin.ts index 20a45c586074a..1f79104b183ee 100644 --- a/src/plugins/tile_map/public/plugin.ts +++ b/src/plugins/tile_map/public/plugin.ts @@ -32,7 +32,7 @@ import 'angular-sanitize'; import { createTileMapFn } from './tile_map_fn'; // @ts-ignore import { createTileMapTypeDefinition } from './tile_map_type'; -import { getBaseMapsVis, MapsLegacyPluginSetup } from '../../maps_legacy/public'; +import { MapsLegacyPluginSetup } from '../../maps_legacy/public'; import { DataPublicPluginStart } from '../../data/public'; import { setFormatService, setQueryService } from './services'; import { setKibanaLegacy } from './services'; @@ -85,7 +85,7 @@ export class TileMapPlugin implements Plugin = { getZoomPrecision, getPrecision, - BaseMapsVisualization: getBaseMapsVis(core, mapsLegacy.serviceSettings), + BaseMapsVisualization: mapsLegacy.getBaseMapsVis(), uiSettings: core.uiSettings, }; diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index 85c87306cc4f9..9fcd8a32881df 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -22,7 +22,7 @@ import { TriggerInternal } from './triggers/trigger_internal'; import { Filter } from '../../data/public'; import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER } from './triggers'; import { IEmbeddable } from '../../embeddable/public'; -import { RangeSelectTriggerContext, ValueClickTriggerContext } from '../../embeddable/public'; +import { RangeSelectContext, ValueClickContext } from '../../embeddable/public'; export type TriggerRegistry = Map>; export type ActionRegistry = Map; @@ -37,8 +37,8 @@ export type TriggerContext = BaseContext; export interface TriggerContextMapping { [DEFAULT_TRIGGER]: TriggerContext; - [SELECT_RANGE_TRIGGER]: RangeSelectTriggerContext; - [VALUE_CLICK_TRIGGER]: ValueClickTriggerContext; + [SELECT_RANGE_TRIGGER]: RangeSelectContext; + [VALUE_CLICK_TRIGGER]: ValueClickContext; [APPLY_FILTER_TRIGGER]: { embeddable: IEmbeddable; filters: Filter[]; diff --git a/src/plugins/vis_default_editor/public/components/agg_group.tsx b/src/plugins/vis_default_editor/public/components/agg_group.tsx index 3030601236687..4cde33b8fbc31 100644 --- a/src/plugins/vis_default_editor/public/components/agg_group.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_group.tsx @@ -152,7 +152,7 @@ function DefaultEditorAggGroup({ {bucketsError && ( <> - {bucketsError} + {bucketsError} )} diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts index 45abbf8d2b2dd..39abddb3de853 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -111,7 +111,11 @@ function getAggParamsToRender({ const aggType = agg.type.type; const aggName = agg.type.name; const aggParams = get(aggParamsMap, [aggType, aggName], {}); - paramEditor = get(aggParams, param.name) || get(aggParamsMap, ['common', param.type]); + paramEditor = get(aggParams, param.name); + } + + if (!paramEditor) { + paramEditor = get(aggParamsMap, ['common', param.type]); } // show params with an editor component diff --git a/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx index 361eeba9abdbf..fc79ba703c2b4 100644 --- a/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx @@ -45,9 +45,10 @@ function SubMetricParamEditor({ defaultMessage: 'Bucket', }); const type = aggParam.name; + const isCustomMetric = type === 'customMetric'; - const aggTitle = type === 'customMetric' ? metricTitle : bucketTitle; - const aggGroup = type === 'customMetric' ? AggGroupNames.Metrics : AggGroupNames.Buckets; + const aggTitle = isCustomMetric ? metricTitle : bucketTitle; + const aggGroup = isCustomMetric ? AggGroupNames.Metrics : AggGroupNames.Buckets; useMount(() => { if (agg.params[type]) { @@ -87,7 +88,7 @@ function SubMetricParamEditor({ setValidity={setValidity} setTouched={setTouched} schemas={schemas} - hideCustomLabel={true} + hideCustomLabel={!isCustomMetric} /> ); diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index c3bc72497007e..80d53021b7866 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -26,6 +26,7 @@ import { tableVisResponseHandler } from './table_vis_response_handler'; import tableVisTemplate from './table_vis.html'; import { TableOptions } from './components/table_vis_options_lazy'; import { getTableVisualizationControllerClass } from './vis_controller'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitializerContext) { return { @@ -39,6 +40,9 @@ export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitia defaultMessage: 'Display values in a table', }), visualization: getTableVisualizationControllerClass(core, context), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, visConfig: { defaults: { perPage: 10, diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts index 5a8cc3004a315..023489c6d2e87 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts @@ -22,6 +22,7 @@ import { i18n } from '@kbn/i18n'; import { Schemas } from '../../vis_default_editor/public'; import { TagCloudOptions } from './components/tag_cloud_options'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; // @ts-ignore import { createTagCloudVisualization } from './components/tag_cloud_visualization'; @@ -31,6 +32,9 @@ export const createTagCloudVisTypeDefinition = (deps: TagCloudVisDependencies) = name: 'tagcloud', title: i18n.translate('visTypeTagCloud.vis.tagCloudTitle', { defaultMessage: 'Tag Cloud' }), icon: 'visTagCloud', + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, description: i18n.translate('visTypeTagCloud.vis.tagCloudDescription', { defaultMessage: 'A group of words, sized according to their importance', }), diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index b3e35dac3711f..c20a104736291 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -33,7 +33,7 @@ import { import { createVegaFn } from './vega_fn'; import { createVegaTypeDefinition } from './vega_type'; -import { getKibanaMapFactoryProvider, IServiceSettings } from '../../maps_legacy/public'; +import { IServiceSettings } from '../../maps_legacy/public'; import './index.scss'; import { ConfigSchema } from '../config'; @@ -77,7 +77,7 @@ export class VegaPlugin implements Plugin, void> { emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), }); setUISettings(core.uiSettings); - setKibanaMapFactory(getKibanaMapFactoryProvider(core)); + setKibanaMapFactory(mapsLegacy.getKibanaMapFactoryProvider); setMapsLegacyConfig(mapsLegacy.config); const visualizationDependencies: Readonly = { diff --git a/src/plugins/vis_type_vislib/public/area.ts b/src/plugins/vis_type_vislib/public/area.ts index c42962ad50a4b..ec90fbd1746a1 100644 --- a/src/plugins/vis_type_vislib/public/area.ts +++ b/src/plugins/vis_type_vislib/public/area.ts @@ -40,6 +40,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'area', @@ -49,6 +50,9 @@ export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => defaultMessage: 'Emphasize the quantity beneath a line chart', }), visualization: createVislibVisController(deps), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; + }, visConfig: { defaults: { type: 'area', diff --git a/src/plugins/vis_type_vislib/public/heatmap.ts b/src/plugins/vis_type_vislib/public/heatmap.ts index ced7a38568ffd..bd3d02029cb23 100644 --- a/src/plugins/vis_type_vislib/public/heatmap.ts +++ b/src/plugins/vis_type_vislib/public/heatmap.ts @@ -28,6 +28,7 @@ import { TimeMarker } from './vislib/visualizations/time_marker'; import { CommonVislibParams, ValueAxis } from './types'; import { VisTypeVislibDependencies } from './plugin'; import { ColorSchemas, ColorSchemaParams } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export interface HeatmapVisParams extends CommonVislibParams, ColorSchemaParams { type: 'heatmap'; @@ -48,6 +49,9 @@ export const createHeatmapVisTypeDefinition = (deps: VisTypeVislibDependencies) description: i18n.translate('visTypeVislib.heatmap.heatmapDescription', { defaultMessage: 'Shade cells within a matrix', }), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, visualization: createVislibVisController(deps), visConfig: { defaults: { diff --git a/src/plugins/vis_type_vislib/public/histogram.ts b/src/plugins/vis_type_vislib/public/histogram.ts index 52242ad11e8f5..8aeeb4ec533ab 100644 --- a/src/plugins/vis_type_vislib/public/histogram.ts +++ b/src/plugins/vis_type_vislib/public/histogram.ts @@ -39,6 +39,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'histogram', @@ -50,6 +51,9 @@ export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies defaultMessage: 'Assign a continuous variable to each axis', }), visualization: createVislibVisController(deps), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; + }, visConfig: { defaults: { type: 'histogram', diff --git a/src/plugins/vis_type_vislib/public/horizontal_bar.ts b/src/plugins/vis_type_vislib/public/horizontal_bar.ts index a58c15f136431..702581828e60d 100644 --- a/src/plugins/vis_type_vislib/public/horizontal_bar.ts +++ b/src/plugins/vis_type_vislib/public/horizontal_bar.ts @@ -37,6 +37,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'horizontal_bar', @@ -48,6 +49,9 @@ export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependen defaultMessage: 'Assign a continuous variable to each axis', }), visualization: createVislibVisController(deps), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; + }, visConfig: { defaults: { type: 'histogram', diff --git a/src/plugins/vis_type_vislib/public/line.ts b/src/plugins/vis_type_vislib/public/line.ts index a94fd3f3945ab..6e9190229114b 100644 --- a/src/plugins/vis_type_vislib/public/line.ts +++ b/src/plugins/vis_type_vislib/public/line.ts @@ -38,6 +38,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'line', @@ -47,6 +48,9 @@ export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => defaultMessage: 'Emphasize trends', }), visualization: createVislibVisController(deps), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; + }, visConfig: { defaults: { type: 'line', diff --git a/src/plugins/vis_type_vislib/public/pie.ts b/src/plugins/vis_type_vislib/public/pie.ts index a68bc5893406f..1e81dbdde3f68 100644 --- a/src/plugins/vis_type_vislib/public/pie.ts +++ b/src/plugins/vis_type_vislib/public/pie.ts @@ -26,6 +26,7 @@ import { getPositions, Positions } from './utils/collections'; import { createVislibVisController } from './vis_controller'; import { CommonVislibParams } from './types'; import { VisTypeVislibDependencies } from './plugin'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export interface PieVisParams extends CommonVislibParams { type: 'pie'; @@ -47,6 +48,9 @@ export const createPieVisTypeDefinition = (deps: VisTypeVislibDependencies) => ( defaultMessage: 'Compare parts of a whole', }), visualization: createVislibVisController(deps), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, visConfig: { defaults: { type: 'pie', diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 26fdd665192a6..2f9cda32fccdc 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -377,29 +377,6 @@ export class VisualizeEmbeddable extends Embeddable Array; icon?: string; image?: string; stage?: 'experimental' | 'beta' | 'production'; @@ -44,6 +46,7 @@ export class BaseVisType { name: string; title: string; description: string; + getSupportedTriggers?: () => Array; icon?: string; image?: string; stage: 'experimental' | 'beta' | 'production'; @@ -77,6 +80,7 @@ export class BaseVisType { this.name = opts.name; this.description = opts.description || ''; + this.getSupportedTriggers = opts.getSupportedTriggers; this.title = opts.title; this.icon = opts.icon; this.image = opts.image; diff --git a/src/plugins/visualizations/public/vis_types/types_service.ts b/src/plugins/visualizations/public/vis_types/types_service.ts index 321f96180fd68..14c2a9c50ab0e 100644 --- a/src/plugins/visualizations/public/vis_types/types_service.ts +++ b/src/plugins/visualizations/public/vis_types/types_service.ts @@ -23,11 +23,13 @@ import { visTypeAliasRegistry, VisTypeAlias } from './vis_type_alias_registry'; import { BaseVisType } from './base_vis_type'; // @ts-ignore import { ReactVisType } from './react_vis_type'; +import { TriggerContextMapping } from '../../../ui_actions/public'; export interface VisType { name: string; title: string; description?: string; + getSupportedTriggers?: () => Array; visualization: any; isAccessible?: boolean; requestHandler: string | unknown; diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts index bc80d549c81e6..f6d27b54c7c64 100644 --- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts +++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { TriggerContextMapping } from '../../../ui_actions/public'; export interface VisualizationListItem { editUrl: string; @@ -26,6 +27,7 @@ export interface VisualizationListItem { savedObjectType: string; title: string; description?: string; + getSupportedTriggers?: () => Array; typeTitle: string; image?: string; } @@ -53,6 +55,7 @@ export interface VisTypeAlias { icon: string; promotion?: VisTypeAliasPromotion; description: string; + getSupportedTriggers?: () => Array; stage: 'experimental' | 'beta' | 'production'; appExtensions?: { diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js index e74cd180185ab..88e6b3a29052e 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.js +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -37,8 +37,17 @@ function flatKeys(source) { export default function ({ getService }) { const supertest = getService('supertest'); + const es = getService('es'); describe('/api/telemetry/v2/clusters/_stats', () => { + before('create some telemetry-data tracked indices', async () => { + return es.indices.create({ index: 'filebeat-telemetry_tests_logs' }); + }); + + after('cleanup telemetry-data tracked indices', () => { + return es.indices.delete({ index: 'filebeat-telemetry_tests_logs' }); + }); + it('should pull local stats and validate data types', async () => { const timeRange = { min: '2018-07-23T22:07:00Z', @@ -71,6 +80,17 @@ export default function ({ getService }) { expect(stats.stack_stats.kibana.plugins.csp.strict).to.be(true); expect(stats.stack_stats.kibana.plugins.csp.warnLegacyBrowsers).to.be(true); expect(stats.stack_stats.kibana.plugins.csp.rulesChangedFromDefault).to.be(false); + + // Testing stack_stats.data + expect(stats.stack_stats.data).to.be.an('object'); + expect(stats.stack_stats.data).to.be.an('array'); + expect(stats.stack_stats.data[0]).to.be.an('object'); + expect(stats.stack_stats.data[0].pattern_name).to.be('filebeat'); + expect(stats.stack_stats.data[0].shipper).to.be('filebeat'); + expect(stats.stack_stats.data[0].index_count).to.be(1); + expect(stats.stack_stats.data[0].doc_count).to.be(0); + expect(stats.stack_stats.data[0].ecs_index_count).to.be(0); + expect(stats.stack_stats.data[0].size_in_bytes).to.be.greaterThan(0); }); it('should pull local stats and validate fields', async () => { diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index de9606f3d02ed..906f0b83e99e7 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -20,6 +20,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { + const browser = getService('browser'); const log = getService('log'); const retry = getService('retry'); const esArchiver = getService('esArchiver'); @@ -268,5 +269,19 @@ export default function ({ getService, getPageObjects }) { expect(toastMessage).to.be('Invalid time range'); }); }); + + describe('managing fields', function () { + it('should add a field, sort by it, remove it and also sorting by it', async function () { + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.clickFieldListItemAdd('_score'); + await PageObjects.discover.clickFieldSort('_score'); + const currentUrlWithScore = await browser.getCurrentUrl(); + expect(currentUrlWithScore).to.contain('_score'); + await PageObjects.discover.clickFieldListItemAdd('_score'); + const currentUrlWithoutScore = await browser.getCurrentUrl(); + expect(currentUrlWithoutScore).not.to.contain('_score'); + }); + }); }); } diff --git a/test/functional/apps/visualize/_line_chart.js b/test/functional/apps/visualize/_line_chart.js index 5c510617fbb01..a492f3858b524 100644 --- a/test/functional/apps/visualize/_line_chart.js +++ b/test/functional/apps/visualize/_line_chart.js @@ -279,5 +279,79 @@ export default function ({ getService, getPageObjects }) { expect(labels).to.eql(expectedLabels); }); }); + + describe('pipeline aggregations', () => { + before(async () => { + log.debug('navigateToApp visualize'); + await PageObjects.visualize.navigateToNewVisualization(); + log.debug('clickLineChart'); + await PageObjects.visualize.clickLineChart(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + }); + + describe('parent pipeline', () => { + it('should have an error if bucket is not selected', async () => { + await PageObjects.visEditor.clickMetricEditor(); + log.debug('Metrics agg = Serial diff'); + await PageObjects.visEditor.selectAggregation('Serial diff', 'metrics'); + await testSubjects.existOrFail('bucketsError'); + }); + + it('should apply with selected bucket', async () => { + log.debug('Bucket = X-axis'); + await PageObjects.visEditor.clickBucket('X-axis'); + log.debug('Aggregation = Date Histogram'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); + expect(title).to.be('Serial Diff of Count'); + }); + + it('should change y-axis label to custom', async () => { + log.debug('set custom label of y-axis to "Custom"'); + await PageObjects.visEditor.setCustomLabel('Custom', 1); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); + expect(title).to.be('Custom'); + }); + + it('should have advanced accordion and json input', async () => { + await testSubjects.click('advancedParams-1'); + await testSubjects.existOrFail('advancedParams-1 > codeEditorContainer'); + }); + }); + + describe('sibling pipeline', () => { + it('should apply with selected bucket', async () => { + log.debug('Metrics agg = Average Bucket'); + await PageObjects.visEditor.selectAggregation('Average Bucket', 'metrics'); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); + expect(title).to.be('Overall Average of Count'); + }); + + it('should change sub metric custom label and calculate y-axis title', async () => { + log.debug('set custom label of sub metric to "Cats"'); + await PageObjects.visEditor.setCustomLabel('Cats', '1-metric'); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); + expect(title).to.be('Overall Average of Cats'); + }); + + it('should outer custom label', async () => { + log.debug('set custom label to "Custom"'); + await PageObjects.visEditor.setCustomLabel('Custom', 1); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); + expect(title).to.be('Custom'); + }); + + it('should have advanced accordion and json input', async () => { + await testSubjects.click('advancedParams-1'); + await testSubjects.existOrFail('advancedParams-1 > codeEditorContainer'); + }); + }); + }); }); } diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 9ba3c9c1c2c88..7e083d41895b6 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -242,6 +242,10 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider return await testSubjects.click(`field-${field}`); } + public async clickFieldSort(field: string) { + return await testSubjects.click(`docTableHeaderFieldSort_${field}`); + } + public async clickFieldListItemAdd(field: string) { await testSubjects.moveMouseTo(`field-${field}`); await testSubjects.click(`fieldToggle-${field}`); diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index fb40b946d7fa3..4b80647c8749d 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -303,6 +303,13 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider ); } + async getAllIndexPatternNames() { + const indexPatterns = await this.getIndexPatternList(); + return await mapAsync(indexPatterns, async (index) => { + return await index.getVisibleText(); + }); + } + async isIndexPatternListEmpty() { await testSubjects.existOrFail('indexPatternTable', { timeout: 5000 }); const indexPatternList = await this.getIndexPatternList(); diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index 20ae89fc1a8d0..7ef291c8c7005 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -52,6 +52,13 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo await this.setAbsoluteRange(this.defaultStartTime, this.defaultEndTime); } + async ensureHiddenNoDataPopover() { + const isVisible = await testSubjects.exists('noDataPopoverDismissButton'); + if (isVisible) { + await testSubjects.click('noDataPopoverDismissButton'); + } + } + /** * the provides a quicker way to set the timepicker to the default range, saves a few seconds */ diff --git a/test/plugin_functional/plugins/core_logging/kibana.json b/test/plugin_functional/plugins/core_logging/kibana.json deleted file mode 100644 index 3289c2c627b9a..0000000000000 --- a/test/plugin_functional/plugins/core_logging/kibana.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "id": "core_logging", - "version": "0.0.1", - "kibanaVersion": "kibana", - "configPath": ["core_logging"], - "server": true -} diff --git a/test/plugin_functional/plugins/core_logging/server/.gitignore b/test/plugin_functional/plugins/core_logging/server/.gitignore deleted file mode 100644 index 9a3d281179193..0000000000000 --- a/test/plugin_functional/plugins/core_logging/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/*debug.log diff --git a/test/plugin_functional/plugins/core_logging/server/plugin.ts b/test/plugin_functional/plugins/core_logging/server/plugin.ts deleted file mode 100644 index a7820a0f67525..0000000000000 --- a/test/plugin_functional/plugins/core_logging/server/plugin.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; -import { Subject } from 'rxjs'; -import { schema } from '@kbn/config-schema'; -import type { - PluginInitializerContext, - Plugin, - CoreSetup, - LoggerContextConfigInput, - Logger, -} from '../../../../../src/core/server'; - -const CUSTOM_LOGGING_CONFIG: LoggerContextConfigInput = { - appenders: { - customJsonFile: { - kind: 'file', - path: resolve(__dirname, 'json_debug.log'), // use 'debug.log' suffix so file watcher does not restart server - layout: { - kind: 'json', - }, - }, - customPatternFile: { - kind: 'file', - path: resolve(__dirname, 'pattern_debug.log'), - layout: { - kind: 'pattern', - pattern: 'CUSTOM - PATTERN [%logger][%level] %message', - }, - }, - }, - - loggers: [ - { context: 'debug_json', appenders: ['customJsonFile'], level: 'debug' }, - { context: 'debug_pattern', appenders: ['customPatternFile'], level: 'debug' }, - { context: 'info_json', appenders: ['customJsonFile'], level: 'info' }, - { context: 'info_pattern', appenders: ['customPatternFile'], level: 'info' }, - { context: 'all', appenders: ['customJsonFile', 'customPatternFile'], level: 'debug' }, - ], -}; - -export class CoreLoggingPlugin implements Plugin { - private readonly logger: Logger; - - constructor(init: PluginInitializerContext) { - this.logger = init.logger.get(); - } - - public setup(core: CoreSetup) { - const loggingConfig$ = new Subject(); - core.logging.configure(loggingConfig$); - - const router = core.http.createRouter(); - - // Expose a route that allows our test suite to write logs as this plugin - router.post( - { - path: '/internal/core-logging/write-log', - validate: { - body: schema.object({ - level: schema.oneOf([schema.literal('debug'), schema.literal('info')]), - message: schema.string(), - context: schema.arrayOf(schema.string()), - }), - }, - }, - (ctx, req, res) => { - const { level, message, context } = req.body; - const logger = this.logger.get(...context); - - if (level === 'debug') { - logger.debug(message); - } else if (level === 'info') { - logger.info(message); - } - - return res.ok(); - } - ); - - // Expose a route to toggle on and off the custom config - router.post( - { - path: '/internal/core-logging/update-config', - validate: { body: schema.object({ enableCustomConfig: schema.boolean() }) }, - }, - (ctx, req, res) => { - if (req.body.enableCustomConfig) { - loggingConfig$.next(CUSTOM_LOGGING_CONFIG); - } else { - loggingConfig$.next({}); - } - - return res.ok({ body: `Updated config: ${req.body.enableCustomConfig}` }); - } - ); - } - - public start() {} - public stop() {} -} diff --git a/test/plugin_functional/plugins/core_logging/tsconfig.json b/test/plugin_functional/plugins/core_logging/tsconfig.json deleted file mode 100644 index 7389eb6ce159b..0000000000000 --- a/test/plugin_functional/plugins/core_logging/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../../../tsconfig.json", - "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true - }, - "include": [ - "index.ts", - "server/**/*.ts", - "../../../../typings/**/*", - ], - "exclude": [] -} diff --git a/test/plugin_functional/test_suites/core_plugins/index.ts b/test/plugin_functional/test_suites/core_plugins/index.ts index 8f7c2267d34b4..8f54ec6c0f4cd 100644 --- a/test/plugin_functional/test_suites/core_plugins/index.ts +++ b/test/plugin_functional/test_suites/core_plugins/index.ts @@ -30,6 +30,5 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) { loadTestFile(require.resolve('./application_leave_confirm')); loadTestFile(require.resolve('./application_status')); loadTestFile(require.resolve('./rendering')); - loadTestFile(require.resolve('./logging')); }); } diff --git a/test/plugin_functional/test_suites/core_plugins/logging.ts b/test/plugin_functional/test_suites/core_plugins/logging.ts deleted file mode 100644 index 9fdaa6ce834ea..0000000000000 --- a/test/plugin_functional/test_suites/core_plugins/logging.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; -import fs from 'fs'; -import expect from '@kbn/expect'; -import { PluginFunctionalProviderContext } from '../../services'; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: PluginFunctionalProviderContext) { - const supertest = getService('supertest'); - - describe('plugin logging', function describeIndexTests() { - const LOG_FILE_DIRECTORY = resolve(__dirname, '..', '..', 'plugins', 'core_logging', 'server'); - const JSON_FILE_PATH = resolve(LOG_FILE_DIRECTORY, 'json_debug.log'); - const PATTERN_FILE_PATH = resolve(LOG_FILE_DIRECTORY, 'pattern_debug.log'); - - beforeEach(async () => { - // "touch" each file to ensure it exists and is empty before each test - await fs.promises.writeFile(JSON_FILE_PATH, ''); - await fs.promises.writeFile(PATTERN_FILE_PATH, ''); - }); - - async function readLines(path: string) { - const contents = await fs.promises.readFile(path, { encoding: 'utf8' }); - return contents.trim().split('\n'); - } - - async function readJsonLines() { - return (await readLines(JSON_FILE_PATH)) - .filter((line) => line.length > 0) - .map((line) => JSON.parse(line)) - .map(({ level, message, context }) => ({ level, message, context })); - } - - function writeLog(context: string[], level: string, message: string) { - return supertest - .post('/internal/core-logging/write-log') - .set('kbn-xsrf', 'anything') - .send({ context, level, message }) - .expect(200); - } - - function setContextConfig(enable: boolean) { - return supertest - .post('/internal/core-logging/update-config') - .set('kbn-xsrf', 'anything') - .send({ enableCustomConfig: enable }) - .expect(200); - } - - it('does not write to custom appenders when not configured', async () => { - await setContextConfig(false); - await writeLog(['debug_json'], 'info', 'i go to the default appender!'); - expect(await readJsonLines()).to.eql([]); - }); - - it('writes debug_json context to custom JSON appender', async () => { - await setContextConfig(true); - await writeLog(['debug_json'], 'debug', 'log1'); - await writeLog(['debug_json'], 'info', 'log2'); - expect(await readJsonLines()).to.eql([ - { - level: 'DEBUG', - context: 'plugins.core_logging.debug_json', - message: 'log1', - }, - { - level: 'INFO', - context: 'plugins.core_logging.debug_json', - message: 'log2', - }, - ]); - }); - - it('writes info_json context to custom JSON appender', async () => { - await setContextConfig(true); - await writeLog(['info_json'], 'debug', 'i should not be logged!'); - await writeLog(['info_json'], 'info', 'log2'); - expect(await readJsonLines()).to.eql([ - { - level: 'INFO', - context: 'plugins.core_logging.info_json', - message: 'log2', - }, - ]); - }); - - it('writes debug_pattern context to custom pattern appender', async () => { - await setContextConfig(true); - await writeLog(['debug_pattern'], 'debug', 'log1'); - await writeLog(['debug_pattern'], 'info', 'log2'); - expect(await readLines(PATTERN_FILE_PATH)).to.eql([ - 'CUSTOM - PATTERN [plugins.core_logging.debug_pattern][DEBUG] log1', - 'CUSTOM - PATTERN [plugins.core_logging.debug_pattern][INFO ] log2', - ]); - }); - - it('writes info_pattern context to custom pattern appender', async () => { - await setContextConfig(true); - await writeLog(['info_pattern'], 'debug', 'i should not be logged!'); - await writeLog(['info_pattern'], 'info', 'log2'); - expect(await readLines(PATTERN_FILE_PATH)).to.eql([ - 'CUSTOM - PATTERN [plugins.core_logging.info_pattern][INFO ] log2', - ]); - }); - - it('writes all context to both appenders', async () => { - await setContextConfig(true); - await writeLog(['all'], 'debug', 'log1'); - await writeLog(['all'], 'info', 'log2'); - expect(await readJsonLines()).to.eql([ - { - level: 'DEBUG', - context: 'plugins.core_logging.all', - message: 'log1', - }, - { - level: 'INFO', - context: 'plugins.core_logging.all', - message: 'log2', - }, - ]); - expect(await readLines(PATTERN_FILE_PATH)).to.eql([ - 'CUSTOM - PATTERN [plugins.core_logging.all][DEBUG] log1', - 'CUSTOM - PATTERN [plugins.core_logging.all][INFO ] log2', - ]); - }); - }); -} diff --git a/test/scripts/jenkins_security_solution_cypress.sh b/test/scripts/jenkins_security_solution_cypress.sh index 8aa3425be0beb..204911a3eedaa 100644 --- a/test/scripts/jenkins_security_solution_cypress.sh +++ b/test/scripts/jenkins_security_solution_cypress.sh @@ -11,16 +11,11 @@ export KIBANA_INSTALL_DIR="$destDir" echo " -> Running security solution cypress tests" cd "$XPACK_DIR" -# Failures across multiple suites, skipping all -# https://github.com/elastic/kibana/issues/69847 -# https://github.com/elastic/kibana/issues/69848 -# https://github.com/elastic/kibana/issues/69849 - -# checks-reporter-with-killswitch "Security solution Cypress Tests" \ -# node scripts/functional_tests \ -# --debug --bail \ -# --kibana-install-dir "$KIBANA_INSTALL_DIR" \ -# --config test/security_solution_cypress/config.ts +checks-reporter-with-killswitch "Security solution Cypress Tests" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ + --config test/security_solution_cypress/config.ts echo "" echo "" diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index 66b16566418b5..e511d7a8fc15e 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -125,31 +125,31 @@ def uploadCombinedReports() { ) } -def ingestData(buildNum, buildUrl, title) { +def ingestData(jobName, buildNum, buildUrl, title) { kibanaPipeline.bash(""" source src/dev/ci_setup/setup_env.sh yarn kbn bootstrap --prefer-offline # Using existing target/kibana-coverage folder - . src/dev/code_coverage/shell_scripts/ingest_coverage.sh ${buildNum} ${buildUrl} + . src/dev/code_coverage/shell_scripts/ingest_coverage.sh '${jobName}' ${buildNum} '${buildUrl}' """, title) } -def ingestWithVault(buildNum, buildUrl, title) { +def ingestWithVault(jobName, buildNum, buildUrl, title) { def vaultSecret = 'secret/kibana-issues/prod/coverage/elasticsearch' withVaultSecret(secret: vaultSecret, secret_field: 'host', variable_name: 'HOST_FROM_VAULT') { withVaultSecret(secret: vaultSecret, secret_field: 'username', variable_name: 'USER_FROM_VAULT') { withVaultSecret(secret: vaultSecret, secret_field: 'password', variable_name: 'PASS_FROM_VAULT') { - ingestData(buildNum, buildUrl, title) + ingestData(jobName, buildNum, buildUrl, title) } } } } -def ingest(timestamp, title) { +def ingest(jobName, buildNumber, buildUrl, timestamp, title) { withEnv([ "TIME_STAMP=${timestamp}", ]) { - ingestWithVault(BUILD_NUMBER, BUILD_URL, title) + ingestWithVault(jobName, buildNumber, buildUrl, title) } } diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx index bfe853241ae1d..2598d66c4976f 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx @@ -8,13 +8,10 @@ import React from 'react'; import { EuiFormRow, EuiFieldText } from '@elastic/eui'; import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public'; import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; -import { - RangeSelectTriggerContext, - ValueClickTriggerContext, -} from '../../../../../src/plugins/embeddable/public'; +import { ChartActionContext } from '../../../../../src/plugins/embeddable/public'; import { CollectConfigProps } from '../../../../../src/plugins/kibana_utils/public'; -export type ActionContext = RangeSelectTriggerContext | ValueClickTriggerContext; +export type ActionContext = ChartActionContext; export interface Config { name: string; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts index 5dfc250a56d28..d8147827ed473 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - RangeSelectTriggerContext, - ValueClickTriggerContext, -} from '../../../../../src/plugins/embeddable/public'; +import { ChartActionContext } from '../../../../../src/plugins/embeddable/public'; import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public'; -export type ActionContext = RangeSelectTriggerContext | ValueClickTriggerContext; +export type ActionContext = ChartActionContext; export interface Config { /** diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx index 5e4ba54864461..037e017097e53 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx @@ -8,10 +8,7 @@ import React from 'react'; import { EuiFormRow, EuiSwitch, EuiFieldText, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public'; import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; -import { - RangeSelectTriggerContext, - ValueClickTriggerContext, -} from '../../../../../src/plugins/embeddable/public'; +import { ChartActionContext } from '../../../../../src/plugins/embeddable/public'; import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public'; function isValidUrl(url: string) { @@ -23,7 +20,7 @@ function isValidUrl(url: string) { } } -export type ActionContext = RangeSelectTriggerContext | ValueClickTriggerContext; +export type ActionContext = ChartActionContext; export interface Config { url: string; diff --git a/x-pack/plugins/apm/common/agent_name.ts b/x-pack/plugins/apm/common/agent_name.ts index 630f5739806af..9d462dad87ec0 100644 --- a/x-pack/plugins/apm/common/agent_name.ts +++ b/x-pack/plugins/apm/common/agent_name.ts @@ -30,10 +30,12 @@ export function isAgentName(agentName: string): agentName is AgentName { return AGENT_NAMES.includes(agentName as AgentName); } +export const RUM_AGENTS = ['js-base', 'rum-js']; + export function isRumAgentName( - agentName: string | undefined + agentName?: string ): agentName is 'js-base' | 'rum-js' { - return agentName === 'js-base' || agentName === 'rum-js'; + return RUM_AGENTS.includes(agentName!); } export function isJavaAgentName( diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index 2f9d8a37376d9..5be8ad141ffd0 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -29,16 +29,13 @@ module.exports = { roots: [`${rootDir}/common`, `${rootDir}/public`, `${rootDir}/server`], collectCoverage: true, collectCoverageFrom: [ + ...jestConfig.collectCoverageFrom, '**/*.{js,mjs,jsx,ts,tsx}', - '!**/{__test__,__snapshots__,__examples__,integration_tests,tests}/**', '!**/*.stories.{js,mjs,ts,tsx}', - '!**/*.test.{js,mjs,ts,tsx}', '!**/dev_docs/**', '!**/e2e/**', - '!**/scripts/**', '!**/target/**', '!**/typings/**', - '!**/mocks/**', ], coverageDirectory: `${rootDir}/target/coverage/jest`, coverageReporters: ['html'], diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index 69699b72a96df..f612ac0d383ef 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -27,7 +27,6 @@ import { ServiceOverview } from '../ServiceOverview'; import { TraceOverview } from '../TraceOverview'; import { RumOverview } from '../RumDashboard'; import { RumOverviewLink } from '../../shared/Links/apm/RumOverviewLink'; -import { I18LABELS } from '../RumDashboard/translations'; function getHomeTabs({ serviceMapEnabled = true, @@ -109,11 +108,7 @@ export function Home({ tab }: Props) { -

- {selectedTab.name === 'rum-overview' - ? I18LABELS.endUserExperience - : 'APM'} -

+

APM

diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index 776f74a169966..df72fa604e4b3 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -22,11 +22,11 @@ const ClFlexGroup = styled(EuiFlexGroup)` export function ClientMetrics() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const { data, status } = useFetcher( (callApmApi) => { - if (start && end) { + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum/client-metrics', params: { @@ -35,7 +35,7 @@ export function ClientMetrics() { }); } }, - [start, end, uiFilters] + [start, end, serviceName, uiFilters] ); const STAT_STYLE = { width: '240px' }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index c6b34c8b76698..7d48cee49b104 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -27,7 +27,7 @@ export interface PercentileRange { export const PageLoadDistribution = () => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const [percentileRange, setPercentileRange] = useState({ min: null, @@ -38,7 +38,7 @@ export const PageLoadDistribution = () => { const { data, status } = useFetcher( (callApmApi) => { - if (start && end) { + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum-client/page-load-distribution', params: { @@ -57,7 +57,14 @@ export const PageLoadDistribution = () => { }); } }, - [end, start, uiFilters, percentileRange.min, percentileRange.max] + [ + end, + start, + serviceName, + uiFilters, + percentileRange.min, + percentileRange.max, + ] ); const onPercentileChange = (min: number, max: number) => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts index 814cf977c9569..805d19e2321d5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts @@ -17,13 +17,13 @@ interface Props { export const useBreakdowns = ({ percentileRange, field, value }: Props) => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const { min: minP, max: maxP } = percentileRange ?? {}; return useFetcher( (callApmApi) => { - if (start && end && field && value) { + if (start && end && serviceName && field && value) { return callApmApi({ pathname: '/api/apm/rum-client/page-load-distribution/breakdown', params: { @@ -43,6 +43,6 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => { }); } }, - [end, start, uiFilters, field, value, minP, maxP] + [end, start, serviceName, uiFilters, field, value, minP, maxP] ); }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index 34347f3f95947..328b873ef8562 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -16,13 +16,13 @@ import { BreakdownItem } from '../../../../../typings/ui_filters'; export const PageViewsTrend = () => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const [breakdowns, setBreakdowns] = useState([]); const { data, status } = useFetcher( (callApmApi) => { - if (start && end) { + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum-client/page-view-trends', params: { @@ -40,7 +40,7 @@ export const PageViewsTrend = () => { }); } }, - [end, start, uiFilters, breakdowns] + [end, start, serviceName, uiFilters, breakdowns] ); const onBreakdownChange = (values: BreakdownItem[]) => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index cd50f3b575113..326d4a00fd31f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -16,50 +16,33 @@ import { ClientMetrics } from './ClientMetrics'; import { PageViewsTrend } from './PageViewsTrend'; import { PageLoadDistribution } from './PageLoadDistribution'; import { I18LABELS } from './translations'; -import { useUrlParams } from '../../../hooks/useUrlParams'; export function RumDashboard() { - const { urlParams } = useUrlParams(); - - const { environment } = urlParams; - - let environmentLabel = environment || 'all environments'; - - if (environment === 'ENVIRONMENT_NOT_DEFINED') { - environmentLabel = 'undefined environment'; - } - return ( - <> - -

{I18LABELS.getWhatIsGoingOn(environmentLabel)}

-
- - - - - - - -

{I18LABELS.pageLoadTimes}

-
- - -
-
-
-
- - - - - - - - - - -
- + + + + + + +

{I18LABELS.pageLoadTimes}

+
+ + +
+
+
+
+ + + + + + + + + + +
); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 8f21065b0dab0..c9e475ef15316 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -4,12 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, +} from '@elastic/eui'; import React, { useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { PROJECTION } from '../../../../common/projections/typings'; import { RumDashboard } from './RumDashboard'; +import { ServiceNameFilter } from '../../shared/LocalUIFilters/ServiceNameFilter'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { RUM_AGENTS } from '../../../../common/agent_name'; export function RumOverview() { useTrackPageview({ app: 'apm', path: 'rum_overview' }); @@ -24,12 +33,42 @@ export function RumOverview() { return config; }, []); + const { + urlParams: { start, end }, + } = useUrlParams(); + + const { data } = useFetcher( + (callApmApi) => { + if (start && end) { + return callApmApi({ + pathname: '/api/apm/services', + params: { + query: { + start, + end, + uiFilters: JSON.stringify({ agentName: RUM_AGENTS }), + }, + }, + }); + } + }, + [start, end] + ); + return ( <> - + + service.serviceName) ?? [] + } + /> + + + diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts index 4da7b59ec7fa5..2784d9bfd8efa 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -7,14 +7,6 @@ import { i18n } from '@kbn/i18n'; export const I18LABELS = { - endUserExperience: i18n.translate('xpack.apm.rum.dashboard.title', { - defaultMessage: 'End User Experience', - }), - getWhatIsGoingOn: (environmentVal: string) => - i18n.translate('xpack.apm.rum.dashboard.environment.title', { - defaultMessage: `What's going on in {environmentVal}?`, - values: { environmentVal }, - }), backEnd: i18n.translate('xpack.apm.rum.dashboard.backend', { defaultMessage: 'Backend', }), diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx new file mode 100644 index 0000000000000..e12a4a4831e17 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx @@ -0,0 +1,74 @@ +/* + * 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, { useEffect } from 'react'; +import { + EuiTitle, + EuiHorizontalRule, + EuiSpacer, + EuiSelect, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { history } from '../../../../utils/history'; +import { fromQuery, toQuery } from '../../Links/url_helpers'; + +interface Props { + serviceNames: string[]; +} + +const ServiceNameFilter = ({ serviceNames }: Props) => { + const { + urlParams: { serviceName }, + } = useUrlParams(); + + const options = serviceNames.map((type) => ({ + text: type, + value: type, + })); + + const updateServiceName = (serviceN: string) => { + const newLocation = { + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + serviceName: serviceN, + }), + }; + history.push(newLocation); + }; + + useEffect(() => { + if (!serviceName && serviceNames.length > 0) { + updateServiceName(serviceNames[0]); + } + }, [serviceNames, serviceName]); + + return ( + <> + +

+ {i18n.translate('xpack.apm.localFilters.titles.serviceName', { + defaultMessage: 'Service name', + })} +

+
+ + + + { + updateServiceName(event.target.value); + }} + /> + + ); +}; + +export { ServiceNameFilter }; diff --git a/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx b/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx index 607cc2fb82f8e..9f72ac6d5916e 100644 --- a/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx @@ -11,7 +11,8 @@ import { } from '@testing-library/react-hooks'; import { useDelayedVisibility } from '.'; -describe('useFetcher', () => { +// Failing: See https://github.com/elastic/kibana/issues/66389 +describe.skip('useFetcher', () => { let hook: RenderHookResult; beforeEach(() => { diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 0e495391c94f2..d24cb29eaf24f 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { lazy } from 'react'; +import { euiThemeVars as theme } from '@kbn/ui-shared-deps/theme'; import { ConfigSchema } from '.'; import { ObservabilityPluginSetup } from '../../observability/public'; import { @@ -42,7 +43,6 @@ import { fetchLandingPageData, hasData, } from './services/rest/observability_dashboard'; -import { getTheme } from './utils/get_theme'; export type ApmPluginSetup = void; export type ApmPluginStart = void; @@ -79,9 +79,6 @@ export class ApmPlugin implements Plugin { pluginSetupDeps.home.featureCatalogue.register(featureCatalogueEntry); if (plugins.observability) { - const theme = getTheme({ - isDarkMode: core.uiSettings.get('theme:darkMode'), - }); plugins.observability.dashboard.register({ appName: 'apm', fetchData: async (params) => { diff --git a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts index 1ee8d79ee99a5..a14d827eeaec5 100644 --- a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts +++ b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts @@ -6,9 +6,7 @@ import { fetchLandingPageData, hasData } from './observability_dashboard'; import * as createCallApmApi from './createCallApmApi'; -import { getTheme } from '../../utils/get_theme'; - -const theme = getTheme({ isDarkMode: false }); +import { euiThemeVars as theme } from '@kbn/ui-shared-deps/theme'; describe('Observability dashboard data', () => { const callApmApiMock = jest.spyOn(createCallApmApi, 'callApmApi'); @@ -60,7 +58,7 @@ describe('Observability dashboard data', () => { transactions: { type: 'number', label: 'Transactions', - value: 6, + value: 2, color: '#6092c0', }, }, @@ -117,5 +115,45 @@ describe('Observability dashboard data', () => { }, }); }); + it('returns transaction stat as 0 when y is undefined', async () => { + callApmApiMock.mockImplementation(() => + Promise.resolve({ + serviceCount: 0, + transactionCoordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], + }) + ); + const response = await fetchLandingPageData( + { + startTime: '1', + endTime: '2', + bucketSize: '3', + }, + { theme } + ); + expect(response).toEqual({ + title: 'APM', + appLink: '/app/apm', + stats: { + services: { + type: 'number', + label: 'Services', + value: 0, + }, + transactions: { + type: 'number', + label: 'Transactions', + value: 0, + color: '#6092c0', + }, + }, + series: { + transactions: { + label: 'Transactions', + coordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], + color: '#6092c0', + }, + }, + }); + }); }); }); diff --git a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts index 4614e06cbd45d..589199221d7a9 100644 --- a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts +++ b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts @@ -5,13 +5,13 @@ */ import { i18n } from '@kbn/i18n'; -import { sum } from 'lodash'; +import mean from 'lodash.mean'; +import { Theme } from '@kbn/ui-shared-deps/theme'; import { ApmFetchDataResponse, FetchDataParams, } from '../../../../observability/public'; import { callApmApi } from './createCallApmApi'; -import { Theme } from '../../utils/get_theme'; interface Options { theme: Theme; @@ -48,7 +48,12 @@ export const fetchLandingPageData = async ( 'xpack.apm.observabilityDashboard.stats.transactions', { defaultMessage: 'Transactions' } ), - value: sum(transactionCoordinates.map((coordinates) => coordinates.y)), + value: + mean( + transactionCoordinates + .map(({ y }) => y) + .filter((y) => y && isFinite(y)) + ) || 0, color: theme.euiColorVis1, }, }, diff --git a/x-pack/plugins/apm/public/utils/get_theme.ts b/x-pack/plugins/apm/public/utils/get_theme.ts deleted file mode 100644 index e5020202b7721..0000000000000 --- a/x-pack/plugins/apm/public/utils/get_theme.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; - -export type Theme = ReturnType; - -export function getTheme({ isDarkMode }: { isDarkMode: boolean }) { - return isDarkMode ? darkTheme : lightTheme; -} diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index c41dff79a916a..2dd8ed01082fd 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -115,7 +115,7 @@ function getMlSetup(context: APMRequestHandlerContext, request: KibanaRequest) { const mlClient = ml.mlClient.asScoped(request).callAsCurrentUser; return { mlSystem: ml.mlSystemProvider(mlClient, request), - anomalyDetectors: ml.anomalyDetectorsProvider(mlClient), + anomalyDetectors: ml.anomalyDetectorsProvider(mlClient, request), mlClient, }; } diff --git a/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts index e78a3c1cec24a..0d1a4274c16dc 100644 --- a/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts +++ b/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts @@ -41,17 +41,18 @@ export async function getTransactionCoordinates({ field: '@timestamp', fixed_interval: bucketSize, min_doc_count: 0, - extended_bounds: { min: start, max: end }, }, }, }, }, }); + const deltaAsMinutes = (end - start) / 1000 / 60; + return ( aggregations?.distribution.buckets.map((bucket) => ({ x: bucket.key, - y: bucket.doc_count, + y: bucket.doc_count / deltaAsMinutes, })) || [] ); } diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts index 7a3d9d94dec8e..9f2483ab8a24e 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts @@ -16,6 +16,7 @@ import { USER_AGENT_DEVICE, USER_AGENT_OS, CLIENT_GEO_COUNTRY_ISO_CODE, + SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; const filtersByName = { @@ -85,6 +86,12 @@ const filtersByName = { }), fieldName: USER_AGENT_OS, }, + serviceName: { + title: i18n.translate('xpack.apm.localFilters.titles.serviceName', { + defaultMessage: 'Service name', + }), + fieldName: SERVICE_NAME, + }, }; export type LocalUIFilterName = keyof typeof filtersByName; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx index 6ce7dccd3a3ec..52b232afa9410 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx @@ -22,8 +22,8 @@ import { createDashboardUrlGenerator } from '../../../../../../../src/plugins/da import { UrlGeneratorsService } from '../../../../../../../src/plugins/share/public/url_generators'; import { VisualizeEmbeddableContract } from '../../../../../../../src/plugins/visualizations/public'; import { - RangeSelectTriggerContext, - ValueClickTriggerContext, + RangeSelectContext, + ValueClickContext, } from '../../../../../../../src/plugins/embeddable/public'; import { StartDependencies } from '../../../plugin'; import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; @@ -136,8 +136,8 @@ describe('.execute() & getHref', () => { const context = ({ data: { ...(useRangeEvent - ? ({ range: {} } as RangeSelectTriggerContext['data']) - : ({ data: [] } as ValueClickTriggerContext['data'])), + ? ({ range: {} } as RangeSelectContext['data']) + : ({ data: [] } as ValueClickContext['data'])), timeFieldName: 'order_date', }, embeddable: { diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts index 1fbff0a7269e2..6be2e2a77269f 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts @@ -5,14 +5,14 @@ */ import { - ValueClickTriggerContext, - RangeSelectTriggerContext, + ValueClickContext, + RangeSelectContext, IEmbeddable, } from '../../../../../../../src/plugins/embeddable/public'; export type ActionContext = - | ValueClickTriggerContext - | RangeSelectTriggerContext; + | ValueClickContext + | RangeSelectContext; export interface Config { dashboardId?: string; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts index 620cabe652778..59359fb35f544 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts @@ -43,14 +43,13 @@ export abstract class AbstractExploreDataAction { if (!embeddable) return false; if (!this.params.start().plugins.discover.urlGenerator) return false; - if (!shared.isVisualizeEmbeddable(embeddable)) return false; - if (!shared.getIndexPattern(embeddable)) return false; + if (!shared.hasExactlyOneIndexPattern(embeddable)) return false; if (embeddable.getInput().viewMode !== ViewMode.VIEW) return false; return true; } public async execute(context: Context): Promise { - if (!shared.isVisualizeEmbeddable(context.embeddable)) return; + if (!shared.hasExactlyOneIndexPattern(context.embeddable)) return; const { core } = this.params.start(); const { appName, appPath } = await this.getUrl(context); @@ -63,7 +62,7 @@ export abstract class AbstractExploreDataAction { const { embeddable } = context; - if (!shared.isVisualizeEmbeddable(embeddable)) { + if (!shared.hasExactlyOneIndexPattern(embeddable)) { throw new Error(`Embeddable not supported for "${this.getDisplayName(context)}" action.`); } diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts index a273f0d50e45e..0d22f0a36d418 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts @@ -10,8 +10,8 @@ import { coreMock } from '../../../../../../src/core/public/mocks'; import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; import { EmbeddableStart, - RangeSelectTriggerContext, - ValueClickTriggerContext, + RangeSelectContext, + ValueClickContext, ChartActionContext, } from '../../../../../../src/plugins/embeddable/public'; import { i18n } from '@kbn/i18n'; @@ -85,8 +85,8 @@ const setup = ({ useRangeEvent = false }: { useRangeEvent?: boolean } = {}) => { const data: ChartActionContext['data'] = { ...(useRangeEvent - ? ({ range: {} } as RangeSelectTriggerContext['data']) - : ({ data: [] } as ValueClickTriggerContext['data'])), + ? ({ range: {} } as RangeSelectContext['data']) + : ({ data: [] } as ValueClickContext['data'])), timeFieldName: 'order_date', }; @@ -139,9 +139,16 @@ describe('"Explore underlying data" panel action', () => { expect(isCompatible).toBe(false); }); - test('returns false if embeddable is not Visualize embeddable', async () => { - const { action, embeddable, context } = setup(); - (embeddable as any).type = 'NOT_VISUALIZE_EMBEDDABLE'; + test('returns false if embeddable has more than one index pattern', async () => { + const { action, output, context } = setup(); + output.indexPatterns = [ + { + id: 'index-ptr-foo', + }, + { + id: 'index-ptr-bar', + }, + ]; const isCompatible = await action.isCompatible(context); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts index 359f14959c6a6..658a6bcb3cf4d 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts @@ -6,8 +6,8 @@ import { Action } from '../../../../../../src/plugins/ui_actions/public'; import { - ValueClickTriggerContext, - RangeSelectTriggerContext, + ValueClickContext, + RangeSelectContext, } from '../../../../../../src/plugins/embeddable/public'; import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public'; @@ -15,7 +15,7 @@ import { KibanaURL } from './kibana_url'; import * as shared from './shared'; import { AbstractExploreDataAction } from './abstract_explore_data_action'; -export type ExploreDataChartActionContext = ValueClickTriggerContext | RangeSelectTriggerContext; +export type ExploreDataChartActionContext = ValueClickContext | RangeSelectContext; export const ACTION_EXPLORE_DATA_CHART = 'ACTION_EXPLORE_DATA_CHART'; @@ -49,7 +49,7 @@ export class ExploreDataChartAction extends AbstractExploreDataAction { expect(isCompatible).toBe(false); }); - test('returns false if embeddable is not Visualize embeddable', async () => { - const { action, embeddable, context } = setup(); - (embeddable as any).type = 'NOT_VISUALIZE_EMBEDDABLE'; + test('returns false if embeddable has more than one index pattern', async () => { + const { action, output, context } = setup(); + output.indexPatterns = [ + { + id: 'index-ptr-foo', + }, + { + id: 'index-ptr-bar', + }, + ]; const isCompatible = await action.isCompatible(context); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts index 6691089f875d8..8b79211a914cc 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts @@ -38,7 +38,7 @@ export class ExploreDataContextMenuAction extends AbstractExploreDataAction } => { if (!output || typeof output !== 'object') return false; return Array.isArray((output as any).indexPatterns); }; -export const isVisualizeEmbeddable = ( - embeddable?: IEmbeddable -): embeddable is VisualizeEmbeddableContract => - embeddable && embeddable?.type === VISUALIZE_EMBEDDABLE_TYPE ? true : false; - -/** - * @returns Returns empty string if no index pattern ID found. - */ -export const getIndexPattern = (embeddable?: IEmbeddable): string => { - if (!embeddable) return ''; +export const getIndexPatterns = (embeddable?: IEmbeddable): string[] => { + if (!embeddable) return []; const output = embeddable.getOutput(); - if (isOutputWithIndexPatterns(output) && output.indexPatterns.length > 0) { - return output.indexPatterns[0].id; - } - - return ''; + return isOutputWithIndexPatterns(output) ? output.indexPatterns.map(({ id }) => id) : []; }; + +export const hasExactlyOneIndexPattern = (embeddable?: IEmbeddable): boolean => + getIndexPatterns(embeddable).length === 1; diff --git a/x-pack/plugins/grokdebugger/public/index.js b/x-pack/plugins/grokdebugger/public/index.js index 960c9d8d58e4a..d97410a2fe355 100644 --- a/x-pack/plugins/grokdebugger/public/index.js +++ b/x-pack/plugins/grokdebugger/public/index.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin } from './plugin'; +import { GrokDebuggerUIPlugin } from './plugin'; export function plugin(initializerContext) { - return new Plugin(initializerContext); + return new GrokDebuggerUIPlugin(initializerContext); } diff --git a/x-pack/plugins/grokdebugger/public/plugin.js b/x-pack/plugins/grokdebugger/public/plugin.js index 6ac600c9dc97b..c83eb85ce4d75 100644 --- a/x-pack/plugins/grokdebugger/public/plugin.js +++ b/x-pack/plugins/grokdebugger/public/plugin.js @@ -6,10 +6,11 @@ import { i18n } from '@kbn/i18n'; import { first } from 'rxjs/operators'; -import { registerFeature } from './register_feature'; + import { PLUGIN } from '../common/constants'; +import { registerFeature } from './register_feature'; -export class Plugin { +export class GrokDebuggerUIPlugin { setup(coreSetup, plugins) { registerFeature(plugins.home); @@ -20,7 +21,7 @@ export class Plugin { }), id: PLUGIN.ID, enableRouting: false, - async mount(context, { element }) { + async mount({ element }) { const [coreStart] = await coreSetup.getStartServices(); const license = await plugins.licensing.license$.pipe(first()).toPromise(); const { renderApp } = await import('./render_app'); diff --git a/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts index ae872cee9aa56..b522d86987283 100644 --- a/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts +++ b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts @@ -42,7 +42,7 @@ export type LogIndexField = rt.TypeOf; const logSourceStatusRT = rt.strict({ logIndexFields: rt.array(logIndexFieldRT), - logIndexNames: rt.array(rt.string), + logIndicesExist: rt.boolean, }); export type LogSourceStatus = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/http_api/snapshot_api.ts b/x-pack/plugins/infra/common/http_api/snapshot_api.ts index 1c7dfed82783a..9ddbcb17089f3 100644 --- a/x-pack/plugins/infra/common/http_api/snapshot_api.ts +++ b/x-pack/plugins/infra/common/http_api/snapshot_api.ts @@ -34,7 +34,7 @@ export const SnapshotNodeMetricRT = rt.intersection([ SnapshotNodeMetricOptionalRT, ]); export const SnapshotNodeRT = rt.type({ - metric: SnapshotNodeMetricRT, + metrics: rt.array(SnapshotNodeMetricRT), path: rt.array(SnapshotNodePathRT), }); @@ -97,7 +97,7 @@ export const SnapshotMetricInputRT = rt.union([ export const SnapshotRequestRT = rt.intersection([ rt.type({ timerange: InfraTimerangeInputRT, - metric: SnapshotMetricInputRT, + metrics: rt.array(SnapshotMetricInputRT), groupBy: SnapshotGroupByRT, nodeType: ItemTypeRT, sourceId: rt.string, diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/index.ts b/x-pack/plugins/infra/common/inventory_models/aws_ec2/index.ts index 5f667beebd83b..c12137f7810d4 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/index.ts @@ -30,4 +30,5 @@ export const awsEC2: InventoryModel = { ip: 'aws.ec2.instance.public.ip', }, requiredMetrics: ['awsEC2CpuUtilization', 'awsEC2NetworkTraffic', 'awsEC2DiskIOBytes'], + tooltipMetrics: ['cpu', 'rx', 'tx'], }; diff --git a/x-pack/plugins/infra/common/inventory_models/aws_rds/index.ts b/x-pack/plugins/infra/common/inventory_models/aws_rds/index.ts index 02cef192b59ef..fa7dd62c0b8f7 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_rds/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_rds/index.ts @@ -35,4 +35,11 @@ export const awsRDS: InventoryModel = { 'awsRDSActiveTransactions', 'awsRDSLatency', ], + tooltipMetrics: [ + 'cpu', + 'rdsLatency', + 'rdsConnections', + 'rdsQueriesExecuted', + 'rdsActiveTransactions', + ], }; diff --git a/x-pack/plugins/infra/common/inventory_models/aws_s3/index.ts b/x-pack/plugins/infra/common/inventory_models/aws_s3/index.ts index a786283a100a9..59c24eb733f9e 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_s3/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_s3/index.ts @@ -35,4 +35,11 @@ export const awsS3: InventoryModel = { 'awsS3DownloadBytes', 'awsS3UploadBytes', ], + tooltipMetrics: [ + 's3BucketSize', + 's3NumberOfObjects', + 's3TotalRequests', + 's3UploadBytes', + 's3DownloadBytes', + ], }; diff --git a/x-pack/plugins/infra/common/inventory_models/aws_sqs/index.ts b/x-pack/plugins/infra/common/inventory_models/aws_sqs/index.ts index 21379ebb1e604..2a9f2ad13d946 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_sqs/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_sqs/index.ts @@ -35,4 +35,11 @@ export const awsSQS: InventoryModel = { 'awsSQSMessagesEmpty', 'awsSQSOldestMessage', ], + tooltipMetrics: [ + 'sqsMessagesVisible', + 'sqsMessagesDelayed', + 'sqsMessagesEmpty', + 'sqsMessagesSent', + 'sqsOldestMessage', + ], }; diff --git a/x-pack/plugins/infra/common/inventory_models/container/index.ts b/x-pack/plugins/infra/common/inventory_models/container/index.ts index 8f2336d11e42b..8c9d6f393b6dd 100644 --- a/x-pack/plugins/infra/common/inventory_models/container/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/container/index.ts @@ -37,4 +37,5 @@ export const container: InventoryModel = { 'containerDiskIOBytes', 'containerDiskIOOps', ], + tooltipMetrics: ['cpu', 'memory', 'rx', 'tx'], }; diff --git a/x-pack/plugins/infra/common/inventory_models/host/index.ts b/x-pack/plugins/infra/common/inventory_models/host/index.ts index 538af4f5119b4..b0bfbd6693e55 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/host/index.ts @@ -47,4 +47,5 @@ export const host: InventoryModel = { ...awsRequiredMetrics, ...nginxRequireMetrics, ], + tooltipMetrics: ['cpu', 'memory', 'tx', 'rx'], }; diff --git a/x-pack/plugins/infra/common/inventory_models/intl_strings.ts b/x-pack/plugins/infra/common/inventory_models/intl_strings.ts index 08949ed53eb10..2a885136f4ee7 100644 --- a/x-pack/plugins/infra/common/inventory_models/intl_strings.ts +++ b/x-pack/plugins/infra/common/inventory_models/intl_strings.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { SnapshotMetricType } from './types'; export const CPUUsage = i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', { defaultMessage: 'CPU usage', }); @@ -68,3 +69,81 @@ export const fieldToName = (field: string) => { }; return LOOKUP[field] || field; }; + +export const SNAPSHOT_METRIC_TRANSLATIONS = { + cpu: i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', { + defaultMessage: 'CPU usage', + }), + + memory: i18n.translate('xpack.infra.waffle.metricOptions.memoryUsageText', { + defaultMessage: 'Memory usage', + }), + + rx: i18n.translate('xpack.infra.waffle.metricOptions.inboundTrafficText', { + defaultMessage: 'Inbound traffic', + }), + + tx: i18n.translate('xpack.infra.waffle.metricOptions.outboundTrafficText', { + defaultMessage: 'Outbound traffic', + }), + + logRate: i18n.translate('xpack.infra.waffle.metricOptions.hostLogRateText', { + defaultMessage: 'Log rate', + }), + + load: i18n.translate('xpack.infra.waffle.metricOptions.loadText', { + defaultMessage: 'Load', + }), + + count: i18n.translate('xpack.infra.waffle.metricOptions.countText', { + defaultMessage: 'Count', + }), + diskIOReadBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOReadBytes', { + defaultMessage: 'Disk Reads', + }), + diskIOWriteBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOWriteBytes', { + defaultMessage: 'Disk Writes', + }), + s3BucketSize: i18n.translate('xpack.infra.waffle.metricOptions.s3BucketSize', { + defaultMessage: 'Bucket Size', + }), + s3TotalRequests: i18n.translate('xpack.infra.waffle.metricOptions.s3TotalRequests', { + defaultMessage: 'Total Requests', + }), + s3NumberOfObjects: i18n.translate('xpack.infra.waffle.metricOptions.s3NumberOfObjects', { + defaultMessage: 'Number of Objects', + }), + s3DownloadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3DownloadBytes', { + defaultMessage: 'Downloads (Bytes)', + }), + s3UploadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3UploadBytes', { + defaultMessage: 'Uploads (Bytes)', + }), + rdsConnections: i18n.translate('xpack.infra.waffle.metricOptions.rdsConnections', { + defaultMessage: 'Connections', + }), + rdsQueriesExecuted: i18n.translate('xpack.infra.waffle.metricOptions.rdsQueriesExecuted', { + defaultMessage: 'Queries Executed', + }), + rdsActiveTransactions: i18n.translate('xpack.infra.waffle.metricOptions.rdsActiveTransactions', { + defaultMessage: 'Active Transactions', + }), + rdsLatency: i18n.translate('xpack.infra.waffle.metricOptions.rdsLatency', { + defaultMessage: 'Latency', + }), + sqsMessagesVisible: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesVisible', { + defaultMessage: 'Messages Available', + }), + sqsMessagesDelayed: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesDelayed', { + defaultMessage: 'Messages Delayed', + }), + sqsMessagesSent: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesSent', { + defaultMessage: 'Messages Added', + }), + sqsMessagesEmpty: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesEmpty', { + defaultMessage: 'Messages Returned Empty', + }), + sqsOldestMessage: i18n.translate('xpack.infra.waffle.metricOptions.sqsOldestMessage', { + defaultMessage: 'Oldest Message', + }), +} as Record; diff --git a/x-pack/plugins/infra/common/inventory_models/pod/index.ts b/x-pack/plugins/infra/common/inventory_models/pod/index.ts index 961e0248c79da..70623175f8c00 100644 --- a/x-pack/plugins/infra/common/inventory_models/pod/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/pod/index.ts @@ -37,4 +37,5 @@ export const pod: InventoryModel = { 'podNetworkTraffic', ...nginxRequiredMetrics, ], + tooltipMetrics: ['cpu', 'memory', 'rx', 'tx'], }; diff --git a/x-pack/plugins/infra/common/inventory_models/shared/components/metrics_and_groupby_toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/shared/components/metrics_and_groupby_toolbar_items.tsx index 9ddf422871d18..a66421fe2fd0e 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/components/metrics_and_groupby_toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/shared/components/metrics_and_groupby_toolbar_items.tsx @@ -6,6 +6,7 @@ import React, { useMemo } from 'react'; import { EuiFlexItem } from '@elastic/eui'; +import { toMetricOpt } from '../../../snapshot_metric_i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { WaffleSortControls } from '../../../../public/pages/metrics/inventory_view/components/waffle/waffle_sort_controls'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -16,7 +17,6 @@ import { WaffleMetricControls } from '../../../../public/pages/metrics/inventory import { WaffleGroupByControls } from '../../../../public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls'; import { toGroupByOpt, - toMetricOpt, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper'; import { SnapshotMetricType } from '../../types'; diff --git a/x-pack/plugins/infra/common/inventory_models/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts index 35d83440812d5..2c6432c3e5286 100644 --- a/x-pack/plugins/infra/common/inventory_models/types.ts +++ b/x-pack/plugins/infra/common/inventory_models/types.ts @@ -351,4 +351,5 @@ export interface InventoryModel { }; metrics: InventoryMetrics; requiredMetrics: InventoryMetric[]; + tooltipMetrics: SnapshotMetricType[]; } diff --git a/x-pack/plugins/infra/common/snapshot_metric_i18n.ts b/x-pack/plugins/infra/common/snapshot_metric_i18n.ts new file mode 100644 index 0000000000000..412c60fd9a1a7 --- /dev/null +++ b/x-pack/plugins/infra/common/snapshot_metric_i18n.ts @@ -0,0 +1,208 @@ +/* + * 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 { SnapshotMetricType } from './inventory_models/types'; + +const Translations = { + CPUUsage: i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', { + defaultMessage: 'CPU usage', + }), + + MemoryUsage: i18n.translate('xpack.infra.waffle.metricOptions.memoryUsageText', { + defaultMessage: 'Memory usage', + }), + + InboundTraffic: i18n.translate('xpack.infra.waffle.metricOptions.inboundTrafficText', { + defaultMessage: 'Inbound traffic', + }), + + OutboundTraffic: i18n.translate('xpack.infra.waffle.metricOptions.outboundTrafficText', { + defaultMessage: 'Outbound traffic', + }), + + LogRate: i18n.translate('xpack.infra.waffle.metricOptions.hostLogRateText', { + defaultMessage: 'Log rate', + }), + + Load: i18n.translate('xpack.infra.waffle.metricOptions.loadText', { + defaultMessage: 'Load', + }), + + Count: i18n.translate('xpack.infra.waffle.metricOptions.countText', { + defaultMessage: 'Count', + }), + DiskIOReadBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOReadBytes', { + defaultMessage: 'Disk Reads', + }), + DiskIOWriteBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOWriteBytes', { + defaultMessage: 'Disk Writes', + }), + s3BucketSize: i18n.translate('xpack.infra.waffle.metricOptions.s3BucketSize', { + defaultMessage: 'Bucket Size', + }), + s3TotalRequests: i18n.translate('xpack.infra.waffle.metricOptions.s3TotalRequests', { + defaultMessage: 'Total Requests', + }), + s3NumberOfObjects: i18n.translate('xpack.infra.waffle.metricOptions.s3NumberOfObjects', { + defaultMessage: 'Number of Objects', + }), + s3DownloadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3DownloadBytes', { + defaultMessage: 'Downloads (Bytes)', + }), + s3UploadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3UploadBytes', { + defaultMessage: 'Uploads (Bytes)', + }), + rdsConnections: i18n.translate('xpack.infra.waffle.metricOptions.rdsConnections', { + defaultMessage: 'Connections', + }), + rdsQueriesExecuted: i18n.translate('xpack.infra.waffle.metricOptions.rdsQueriesExecuted', { + defaultMessage: 'Queries Executed', + }), + rdsActiveTransactions: i18n.translate('xpack.infra.waffle.metricOptions.rdsActiveTransactions', { + defaultMessage: 'Active Transactions', + }), + rdsLatency: i18n.translate('xpack.infra.waffle.metricOptions.rdsLatency', { + defaultMessage: 'Latency', + }), + sqsMessagesVisible: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesVisible', { + defaultMessage: 'Messages Available', + }), + sqsMessagesDelayed: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesDelayed', { + defaultMessage: 'Messages Delayed', + }), + sqsMessagesSent: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesSent', { + defaultMessage: 'Messages Added', + }), + sqsMessagesEmpty: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesEmpty', { + defaultMessage: 'Messages Returned Empty', + }), + sqsOldestMessage: i18n.translate('xpack.infra.waffle.metricOptions.sqsOldestMessage', { + defaultMessage: 'Oldest Message', + }), +}; + +export const toMetricOpt = ( + metric: SnapshotMetricType +): { text: string; value: SnapshotMetricType } | undefined => { + switch (metric) { + case 'cpu': + return { + text: Translations.CPUUsage, + value: 'cpu', + }; + case 'memory': + return { + text: Translations.MemoryUsage, + value: 'memory', + }; + case 'rx': + return { + text: Translations.InboundTraffic, + value: 'rx', + }; + case 'tx': + return { + text: Translations.OutboundTraffic, + value: 'tx', + }; + case 'logRate': + return { + text: Translations.LogRate, + value: 'logRate', + }; + case 'load': + return { + text: Translations.Load, + value: 'load', + }; + + case 'count': + return { + text: Translations.Count, + value: 'count', + }; + case 'diskIOReadBytes': + return { + text: Translations.DiskIOReadBytes, + value: 'diskIOReadBytes', + }; + case 'diskIOWriteBytes': + return { + text: Translations.DiskIOWriteBytes, + value: 'diskIOWriteBytes', + }; + case 's3BucketSize': + return { + text: Translations.s3BucketSize, + value: 's3BucketSize', + }; + case 's3TotalRequests': + return { + text: Translations.s3TotalRequests, + value: 's3TotalRequests', + }; + case 's3NumberOfObjects': + return { + text: Translations.s3NumberOfObjects, + value: 's3NumberOfObjects', + }; + case 's3DownloadBytes': + return { + text: Translations.s3DownloadBytes, + value: 's3DownloadBytes', + }; + case 's3UploadBytes': + return { + text: Translations.s3UploadBytes, + value: 's3UploadBytes', + }; + case 'rdsConnections': + return { + text: Translations.rdsConnections, + value: 'rdsConnections', + }; + case 'rdsQueriesExecuted': + return { + text: Translations.rdsQueriesExecuted, + value: 'rdsQueriesExecuted', + }; + case 'rdsActiveTransactions': + return { + text: Translations.rdsActiveTransactions, + value: 'rdsActiveTransactions', + }; + case 'rdsLatency': + return { + text: Translations.rdsLatency, + value: 'rdsLatency', + }; + case 'sqsMessagesVisible': + return { + text: Translations.sqsMessagesVisible, + value: 'sqsMessagesVisible', + }; + case 'sqsMessagesDelayed': + return { + text: Translations.sqsMessagesDelayed, + value: 'sqsMessagesDelayed', + }; + case 'sqsMessagesSent': + return { + text: Translations.sqsMessagesSent, + value: 'sqsMessagesSent', + }; + case 'sqsMessagesEmpty': + return { + text: Translations.sqsMessagesEmpty, + value: 'sqsMessagesEmpty', + }; + case 'sqsOldestMessage': + return { + text: Translations.sqsOldestMessage, + value: 'sqsOldestMessage', + }; + } +}; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index 7df52ad56aef6..d0b4045949d3e 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -16,9 +16,13 @@ import { EuiFormRow, EuiButtonEmpty, EuiFieldSearch, + EuiCheckbox, + EuiToolTip, + EuiIcon, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; import { AlertPreview } from '../../common'; import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; import { @@ -38,7 +42,6 @@ import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/ap // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; -import { toMetricOpt } from '../../../pages/metrics/inventory_view/components/toolbars/toolbar_wrapper'; import { sqsMetricTypes } from '../../../../common/inventory_models/aws_sqs/toolbar_items'; import { ec2MetricTypes } from '../../../../common/inventory_models/aws_ec2/toolbar_items'; import { s3MetricTypes } from '../../../../common/inventory_models/aws_s3/toolbar_items'; @@ -73,6 +76,7 @@ interface Props { filterQuery?: string; filterQueryText?: string; sourceId?: string; + alertOnNoData?: boolean; }; alertInterval: string; alertsContext: AlertsContextValue; @@ -306,6 +310,28 @@ export const Expressions: React.FC = (props) => { + + + {i18n.translate('xpack.infra.metrics.alertFlyout.alertOnNoData', { + defaultMessage: "Alert me if there's no data", + })}{' '} + + + + + } + checked={alertParams.alertOnNoData} + onChange={(e) => setAlertParams('alertOnNoData', e.target.checked)} + /> + (sourceStatus?.logIndexNames?.length ?? 0) > 0, [ - sourceStatus, - ]); - const derivedIndexPattern = useMemo( () => ({ fields: sourceStatus?.logIndexFields ?? [], @@ -160,7 +155,6 @@ export const useLogSource = ({ loadSourceFailureMessage, loadSourceConfiguration, loadSourceStatus, - logIndicesExist, sourceConfiguration, sourceId, sourceStatus, diff --git a/x-pack/plugins/infra/public/lib/lib.ts b/x-pack/plugins/infra/public/lib/lib.ts index 93f7ef644f795..782f6ce5e0eb5 100644 --- a/x-pack/plugins/infra/public/lib/lib.ts +++ b/x-pack/plugins/infra/public/lib/lib.ts @@ -22,7 +22,7 @@ export interface InfraWaffleMapNode { name: string; ip?: string | null; path: SnapshotNodePath[]; - metric: SnapshotNodeMetric; + metrics: SnapshotNodeMetric[]; } export type InfraWaffleMapGroup = InfraWaffleMapGroupOfNodes | InfraWaffleMapGroupOfGroups; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx index 40ac5c74a6836..b2a4ce65ab2b6 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx @@ -18,14 +18,14 @@ export const StreamPageContent: React.FunctionComponent = () => { isUninitialized, loadSource, loadSourceFailureMessage, - logIndicesExist, + sourceStatus, } = useLogSourceContext(); if (isLoading || isUninitialized) { return ; } else if (hasFailedLoadingSource) { return ; - } else if (logIndicesExist) { + } else if (sourceStatus?.logIndicesExist) { return ; } else { return ; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx index 428a7d3fdfe4b..82c21f663bc96 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx @@ -104,10 +104,10 @@ const LogHighlightsStateProvider: React.FC = ({ children }) => { }; export const LogsPageProviders: React.FunctionComponent = ({ children }) => { - const { logIndicesExist } = useLogSourceContext(); + const { sourceStatus } = useLogSourceContext(); // The providers assume the source is loaded, so short-circuit them otherwise - if (!logIndicesExist) { + if (!sourceStatus?.logIndicesExist) { return <>{children}; } diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 3884ee5b7279a..fddd92128708a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -9,7 +9,8 @@ import { useInterval } from 'react-use'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { convertIntervalToString } from '../../../../utils/convert_interval_to_string'; -import { NodesOverview, calculateBoundsFromNodes } from './nodes_overview'; +import { NodesOverview } from './nodes_overview'; +import { calculateBoundsFromNodes } from '../lib/calculate_bounds_from_nodes'; import { PageContent } from '../../../../components/page'; import { useSnapshot } from '../hooks/use_snaphot'; import { useWaffleTimeContext } from '../hooks/use_waffle_time'; @@ -48,7 +49,7 @@ export const Layout = () => { const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext(); const { loading, nodes, reload, interval } = useSnapshot( filterQueryAsJson, - metric, + [metric], groupBy, nodeType, sourceId, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx index db5949f916ff4..723e8e581cdaa 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx @@ -5,7 +5,6 @@ */ import { i18n } from '@kbn/i18n'; -import { max, min } from 'lodash'; import React, { useCallback } from 'react'; import { InventoryItemType } from '../../../../../common/inventory_models/types'; @@ -16,6 +15,7 @@ import { InfraLoadingPanel } from '../../../../components/loading'; import { Map } from './waffle/map'; import { TableView } from './table_view'; import { SnapshotNode } from '../../../../../common/http_api/snapshot_api'; +import { calculateBoundsFromNodes } from '../lib/calculate_bounds_from_nodes'; export interface KueryFilterQuery { kind: 'kuery'; @@ -36,18 +36,6 @@ interface Props { formatter: InfraFormatter; } -export const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => { - const maxValues = nodes.map((node) => node.metric.max); - const minValues = nodes.map((node) => node.metric.value); - // if there is only one value then we need to set the bottom range to zero for min - // otherwise the legend will look silly since both values are the same for top and - // bottom. - if (minValues.length === 1) { - minValues.unshift(0); - } - return { min: min(minValues) || 0, max: max(maxValues) || 0 }; -}; - export const NodesOverview = ({ autoBounds, boundsOverride, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/table_view.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/table_view.tsx index 764eeb154d346..1d94ab2f2f410 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/table_view.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/table_view.tsx @@ -7,7 +7,7 @@ import { EuiButtonEmpty, EuiInMemoryTable, EuiToolTip, EuiBasicTableColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { last } from 'lodash'; +import { last, first } from 'lodash'; import React, { useState, useCallback, useEffect } from 'react'; import { createWaffleMapNode } from '../lib/nodes_to_wafflemap'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../lib/lib'; @@ -142,6 +142,7 @@ export const TableView = (props: Props) => { const items = nodes.map((node) => { const name = last(node.path); + const metric = first(node.metrics); return { name: (name && name.label) || 'unknown', ...getGroupPaths(node.path).reduce( @@ -151,9 +152,9 @@ export const TableView = (props: Props) => { }), {} ), - value: node.metric.value, - avg: node.metric.avg, - max: node.metric.max, + value: (metric && metric.value) || 0, + avg: (metric && metric.avg) || 0, + max: (metric && metric.max) || 0, node: createWaffleMapNode(node), }; }); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx index 7dc92c7a56abf..449c0a89b4642 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx @@ -6,8 +6,6 @@ import React from 'react'; import { EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { SnapshotMetricType } from '../../../../../../common/inventory_models/types'; import { fieldToName } from '../../lib/field_to_display_name'; import { useSourceContext } from '../../../../../containers/source'; import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; @@ -68,208 +66,7 @@ export const ToolbarWrapper = (props: Props) => { ); }; -const ToolbarTranslations = { - CPUUsage: i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', { - defaultMessage: 'CPU usage', - }), - - MemoryUsage: i18n.translate('xpack.infra.waffle.metricOptions.memoryUsageText', { - defaultMessage: 'Memory usage', - }), - - InboundTraffic: i18n.translate('xpack.infra.waffle.metricOptions.inboundTrafficText', { - defaultMessage: 'Inbound traffic', - }), - - OutboundTraffic: i18n.translate('xpack.infra.waffle.metricOptions.outboundTrafficText', { - defaultMessage: 'Outbound traffic', - }), - - LogRate: i18n.translate('xpack.infra.waffle.metricOptions.hostLogRateText', { - defaultMessage: 'Log rate', - }), - - Load: i18n.translate('xpack.infra.waffle.metricOptions.loadText', { - defaultMessage: 'Load', - }), - - Count: i18n.translate('xpack.infra.waffle.metricOptions.countText', { - defaultMessage: 'Count', - }), - DiskIOReadBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOReadBytes', { - defaultMessage: 'Disk Reads', - }), - DiskIOWriteBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOWriteBytes', { - defaultMessage: 'Disk Writes', - }), - s3BucketSize: i18n.translate('xpack.infra.waffle.metricOptions.s3BucketSize', { - defaultMessage: 'Bucket Size', - }), - s3TotalRequests: i18n.translate('xpack.infra.waffle.metricOptions.s3TotalRequests', { - defaultMessage: 'Total Requests', - }), - s3NumberOfObjects: i18n.translate('xpack.infra.waffle.metricOptions.s3NumberOfObjects', { - defaultMessage: 'Number of Objects', - }), - s3DownloadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3DownloadBytes', { - defaultMessage: 'Downloads (Bytes)', - }), - s3UploadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3UploadBytes', { - defaultMessage: 'Uploads (Bytes)', - }), - rdsConnections: i18n.translate('xpack.infra.waffle.metricOptions.rdsConnections', { - defaultMessage: 'Connections', - }), - rdsQueriesExecuted: i18n.translate('xpack.infra.waffle.metricOptions.rdsQueriesExecuted', { - defaultMessage: 'Queries Executed', - }), - rdsActiveTransactions: i18n.translate('xpack.infra.waffle.metricOptions.rdsActiveTransactions', { - defaultMessage: 'Active Transactions', - }), - rdsLatency: i18n.translate('xpack.infra.waffle.metricOptions.rdsLatency', { - defaultMessage: 'Latency', - }), - sqsMessagesVisible: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesVisible', { - defaultMessage: 'Messages Available', - }), - sqsMessagesDelayed: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesDelayed', { - defaultMessage: 'Messages Delayed', - }), - sqsMessagesSent: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesSent', { - defaultMessage: 'Messages Added', - }), - sqsMessagesEmpty: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesEmpty', { - defaultMessage: 'Messages Returned Empty', - }), - sqsOldestMessage: i18n.translate('xpack.infra.waffle.metricOptions.sqsOldestMessage', { - defaultMessage: 'Oldest Message', - }), -}; - export const toGroupByOpt = (field: string) => ({ text: fieldToName(field), field, }); - -export const toMetricOpt = ( - metric: SnapshotMetricType -): { text: string; value: SnapshotMetricType } | undefined => { - switch (metric) { - case 'cpu': - return { - text: ToolbarTranslations.CPUUsage, - value: 'cpu', - }; - case 'memory': - return { - text: ToolbarTranslations.MemoryUsage, - value: 'memory', - }; - case 'rx': - return { - text: ToolbarTranslations.InboundTraffic, - value: 'rx', - }; - case 'tx': - return { - text: ToolbarTranslations.OutboundTraffic, - value: 'tx', - }; - case 'logRate': - return { - text: ToolbarTranslations.LogRate, - value: 'logRate', - }; - case 'load': - return { - text: ToolbarTranslations.Load, - value: 'load', - }; - - case 'count': - return { - text: ToolbarTranslations.Count, - value: 'count', - }; - case 'diskIOReadBytes': - return { - text: ToolbarTranslations.DiskIOReadBytes, - value: 'diskIOReadBytes', - }; - case 'diskIOWriteBytes': - return { - text: ToolbarTranslations.DiskIOWriteBytes, - value: 'diskIOWriteBytes', - }; - case 's3BucketSize': - return { - text: ToolbarTranslations.s3BucketSize, - value: 's3BucketSize', - }; - case 's3TotalRequests': - return { - text: ToolbarTranslations.s3TotalRequests, - value: 's3TotalRequests', - }; - case 's3NumberOfObjects': - return { - text: ToolbarTranslations.s3NumberOfObjects, - value: 's3NumberOfObjects', - }; - case 's3DownloadBytes': - return { - text: ToolbarTranslations.s3DownloadBytes, - value: 's3DownloadBytes', - }; - case 's3UploadBytes': - return { - text: ToolbarTranslations.s3UploadBytes, - value: 's3UploadBytes', - }; - case 'rdsConnections': - return { - text: ToolbarTranslations.rdsConnections, - value: 'rdsConnections', - }; - case 'rdsQueriesExecuted': - return { - text: ToolbarTranslations.rdsQueriesExecuted, - value: 'rdsQueriesExecuted', - }; - case 'rdsActiveTransactions': - return { - text: ToolbarTranslations.rdsActiveTransactions, - value: 'rdsActiveTransactions', - }; - case 'rdsLatency': - return { - text: ToolbarTranslations.rdsLatency, - value: 'rdsLatency', - }; - case 'sqsMessagesVisible': - return { - text: ToolbarTranslations.sqsMessagesVisible, - value: 'sqsMessagesVisible', - }; - case 'sqsMessagesDelayed': - return { - text: ToolbarTranslations.sqsMessagesDelayed, - value: 'sqsMessagesDelayed', - }; - case 'sqsMessagesSent': - return { - text: ToolbarTranslations.sqsMessagesSent, - value: 'sqsMessagesSent', - }; - case 'sqsMessagesEmpty': - return { - text: ToolbarTranslations.sqsMessagesEmpty, - value: 'sqsMessagesEmpty', - }; - case 'sqsOldestMessage': - return { - text: ToolbarTranslations.sqsOldestMessage, - value: 'sqsOldestMessage', - }; - } -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/__snapshots__/conditional_tooltip.test.tsx.snap b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/__snapshots__/conditional_tooltip.test.tsx.snap new file mode 100644 index 0000000000000..b8cdc0acac1dc --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/__snapshots__/conditional_tooltip.test.tsx.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConditionalToolTip should just work 1`] = ` +
+
+ host-01 +
+ + + CPU usage + + + 10% + + + + + Memory usage + + + 80% + + + + + Outbound traffic + + + 8Mbit/s + + + + + Inbound traffic + + + 8Mbit/s + + +
+`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx new file mode 100644 index 0000000000000..d2c30a4f38ee9 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx @@ -0,0 +1,155 @@ +/* + * 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 { mount } from 'enzyme'; +// import { act } from 'react-dom/test-utils'; +import { EuiThemeProvider } from '../../../../../../../observability/public'; +import { EuiToolTip } from '@elastic/eui'; +import { ConditionalToolTip } from './conditional_tooltip'; +import { + InfraWaffleMapNode, + InfraWaffleMapOptions, + InfraFormatterType, +} from '../../../../../lib/lib'; + +jest.mock('../../../../../containers/source', () => ({ + useSourceContext: () => ({ sourceId: 'default' }), +})); + +jest.mock('../../hooks/use_snaphot'); +import { useSnapshot } from '../../hooks/use_snaphot'; +const mockedUseSnapshot = useSnapshot as jest.Mock>; + +const NODE: InfraWaffleMapNode = { + pathId: 'host-01', + id: 'host-01', + name: 'host-01', + path: [{ value: 'host-01', label: 'host-01' }], + metrics: [{ name: 'cpu' }], +}; + +const OPTIONS: InfraWaffleMapOptions = { + formatter: InfraFormatterType.percent, + formatTemplate: '{value}', + metric: { type: 'cpu' }, + groupBy: [], + legend: { + type: 'steppedGradient', + rules: [], + }, + sort: { by: 'value', direction: 'desc' }, +}; + +export const nextTick = () => new Promise((res) => process.nextTick(res)); +const ChildComponent = () =>
child
; + +describe('ConditionalToolTip', () => { + afterEach(() => { + mockedUseSnapshot.mockReset(); + }); + + function createWrapper(currentTime: number = Date.now(), hidden: boolean = false) { + return mount( + + + + ); + } + + it('should return children when hidden', () => { + mockedUseSnapshot.mockReturnValue({ + nodes: [], + error: null, + loading: true, + interval: '', + reload: jest.fn(() => Promise.resolve()), + }); + const currentTime = Date.now(); + const wrapper = createWrapper(currentTime, true); + expect(wrapper.find(ChildComponent).exists()).toBeTruthy(); + }); + + it('should just work', () => { + jest.useFakeTimers(); + const reloadMock = jest.fn(() => Promise.resolve()); + mockedUseSnapshot.mockReturnValue({ + nodes: [ + { + path: [{ label: 'host-01', value: 'host-01', ip: '192.168.1.10' }], + metrics: [ + { name: 'cpu', value: 0.1, avg: 0.4, max: 0.7 }, + { name: 'memory', value: 0.8, avg: 0.8, max: 1 }, + { name: 'tx', value: 1000000, avg: 1000000, max: 1000000 }, + { name: 'rx', value: 1000000, avg: 1000000, max: 1000000 }, + ], + }, + ], + error: null, + loading: false, + interval: '60s', + reload: reloadMock, + }); + const currentTime = Date.now(); + const wrapper = createWrapper(currentTime, false); + expect(wrapper.find(ChildComponent).exists()).toBeTruthy(); + expect(wrapper.find(EuiToolTip).exists()).toBeTruthy(); + const expectedQuery = JSON.stringify({ + bool: { + filter: { + match_phrase: { 'host.name': 'host-01' }, + }, + }, + }); + const expectedMetrics = [{ type: 'cpu' }, { type: 'memory' }, { type: 'tx' }, { type: 'rx' }]; + expect(mockedUseSnapshot).toBeCalledWith( + expectedQuery, + expectedMetrics, + [], + 'host', + 'default', + currentTime, + '', + '', + false + ); + wrapper.find('[data-test-subj~="conditionalTooltipMouseHandler"]').simulate('mouseOver'); + wrapper.find(EuiToolTip).simulate('mouseOver'); + jest.advanceTimersByTime(500); + expect(reloadMock).toHaveBeenCalled(); + expect(wrapper.find(EuiToolTip).props().content).toMatchSnapshot(); + }); + + it('should not load data if mouse out before 200 ms', () => { + jest.useFakeTimers(); + const reloadMock = jest.fn(() => Promise.resolve()); + mockedUseSnapshot.mockReturnValue({ + nodes: [], + error: null, + loading: true, + interval: '', + reload: reloadMock, + }); + const currentTime = Date.now(); + const wrapper = createWrapper(currentTime, false); + expect(wrapper.find(ChildComponent).exists()).toBeTruthy(); + expect(wrapper.find(EuiToolTip).exists()).toBeTruthy(); + wrapper.find('[data-test-subj~="conditionalTooltipMouseHandler"]').simulate('mouseOver'); + jest.advanceTimersByTime(100); + wrapper.find('[data-test-subj~="conditionalTooltipMouseHandler"]').simulate('mouseOut'); + jest.advanceTimersByTime(200); + expect(reloadMock).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx index eda74da708c8f..11f27f6401a31 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx @@ -3,18 +3,117 @@ * 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 { EuiToolTip, EuiToolTipProps } from '@elastic/eui'; -import { omit } from 'lodash'; +import React, { useCallback, useState, useEffect } from 'react'; +import { EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { first } from 'lodash'; +import { withTheme, EuiTheme } from '../../../../../../../observability/public'; +import { useSourceContext } from '../../../../../containers/source'; +import { findInventoryModel } from '../../../../../../common/inventory_models'; +import { + InventoryItemType, + SnapshotMetricType, +} from '../../../../../../common/inventory_models/types'; +import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib'; +import { useSnapshot } from '../../hooks/use_snaphot'; +import { createInventoryMetricFormatter } from '../../lib/create_inventory_metric_formatter'; +import { SNAPSHOT_METRIC_TRANSLATIONS } from '../../../../../../common/inventory_models/intl_strings'; -interface Props extends EuiToolTipProps { +export interface Props { + currentTime: number; hidden: boolean; + node: InfraWaffleMapNode; + options: InfraWaffleMapOptions; + formatter: (val: number) => string; + children: React.ReactElement; + nodeType: InventoryItemType; + theme: EuiTheme | undefined; } -export const ConditionalToolTip = (props: Props) => { - if (props.hidden) { - return props.children; +export const ConditionalToolTip = withTheme( + ({ theme, hidden, node, children, nodeType, currentTime }: Props) => { + const { sourceId } = useSourceContext(); + const [timer, setTimer] = useState | null>(null); + const model = findInventoryModel(nodeType); + const requestMetrics = model.tooltipMetrics.map((type) => ({ type })) as Array<{ + type: SnapshotMetricType; + }>; + const query = JSON.stringify({ + bool: { + filter: { + match_phrase: { [model.fields.id]: node.id }, + }, + }, + }); + + const { nodes, reload } = useSnapshot( + query, + requestMetrics, + [], + nodeType, + sourceId, + currentTime, + '', + '', + false // Doesn't send request until reload() is called + ); + + const handleDataLoad = useCallback(() => { + const id = setTimeout(reload, 200); + setTimer(id); + }, [reload]); + + const cancelDataLoad = useCallback(() => { + return (timer && clearTimeout(timer)) || void 0; + }, [timer]); + + useEffect(() => { + return cancelDataLoad; + }, [timer, cancelDataLoad]); + + if (hidden) { + return children; + } + + const dataNode = first(nodes); + const metrics = (dataNode && dataNode.metrics) || []; + const content = ( +
+
+ {node.name} +
+ {metrics.map((metric) => { + const name = SNAPSHOT_METRIC_TRANSLATIONS[metric.name] || metric.name; + const formatter = createInventoryMetricFormatter({ type: metric.name }); + return ( + + {name} + + {(metric.value && formatter(metric.value)) || '-'} + + + ); + })} +
+ ); + + return ( + +
+ {children} +
+
+ ); } - const propsWithoutHidden = omit(props, 'hidden'); - return {props.children}; -}; +); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx index e7bee82a9f0fe..cc177b895ca50 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { first } from 'lodash'; import { ConditionalToolTip } from './conditional_tooltip'; import { euiStyled } from '../../../../../../../observability/public'; import { @@ -41,7 +42,7 @@ export const Node = class extends React.PureComponent { public render() { const { nodeType, node, options, squareSize, bounds, formatter, currentTime } = this.props; const { isPopoverOpen } = this.state; - const { metric } = node; + const metric = first(node.metrics); const valueMode = squareSize > 70; const ellipsisMode = squareSize > 30; const rawValue = (metric && metric.value) || 0; @@ -62,10 +63,12 @@ export const Node = class extends React.PureComponent { popoverPosition="downCenter" >