diff --git a/packages/eslint-config-kibana/typescript.js b/packages/eslint-config-kibana/typescript.js index 270614ed84b69e..a55ca9391011d3 100644 --- a/packages/eslint-config-kibana/typescript.js +++ b/packages/eslint-config-kibana/typescript.js @@ -124,7 +124,6 @@ module.exports = { }], '@typescript-eslint/no-var-requires': 'error', '@typescript-eslint/unified-signatures': 'error', - '@typescript-eslint/prefer-ts-expect-error': 'warn', 'constructor-super': 'error', 'dot-notation': 'error', 'eqeqeq': ['error', 'always', {'null': 'ignore'}], diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index aaa46ab74714f1..02b64157686c19 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 596c31820e80d0..40e89f199b6a13 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 00000000000000..ca4714779d39e1 --- /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 5aa0f45e4100d4..cef9a442d17bc0 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 831e1e55573b3e..c81da4689052a5 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 0fe3c1f083cf08..1b894bc400f082 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/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index c277dc85e5e048..46fd2b00c23044 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 7441e753efa6ab..5d1261e697bc06 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 26704f46a509c4..452d1954b6e235 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/i18n/integrate_locale_files.test.ts b/src/dev/i18n/integrate_locale_files.test.ts index 7ff1d87f1bc55b..3bd3dc61c044f4 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 d8ccccca15559b..f9cd6dd1971c75 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 5900bf5aff2524..afaf3cd875a8a0 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 1d1c3118e08526..11a002fdbf4a86 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/run_i18n_check.ts b/src/dev/run_i18n_check.ts index 97ea988b1de3aa..70eeedac2b8b6a 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 23d66fae9f26e4..25c3ea32783aa8 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 17610702a0bc72..30e7587707d2ea 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 49a232ce35cd0c..851dc7a063d7b4 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 { '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 8c538288d2feaf..8e907d4b0cb883 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 8e8054ac204d9f..719827a98cc634 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/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 32295745ce2179..120bbf3b68f7bd 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/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index 1a9d6bf4848f41..788ec1f145e2a5 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 943cca286a722f..932897cdd78617 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 130d07b441b83c..3ee44aaa0816eb 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/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 a0de79da565e62..8d6a2d110efe0f 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 cbe8b9213d577d..6b9c7d1c52db93 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 e0a6a6e21ab007..256b5f386d5f7a 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 2d1a45beb5d875..2d78fdc246e197 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 7c2b841e4adf3a..f4f88bd5807d51 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 78c2498b9ee900..6b4e06fec9ccc3 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 3dcfc7c2fc6fae..0a2a18c7cef4f4 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 6b31de758a4caa..04a2ba2f23f4eb 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 00000000000000..2d0864b1cb75f8 --- /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 00000000000000..8bffc5d012a741 --- /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 00000000000000..cf906bc5c86cfc --- /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/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts new file mode 100644 index 00000000000000..d056d1c9f299f3 --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts @@ -0,0 +1,27 @@ +/* + * 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 { DATA_TELEMETRY_ID } from './constants'; + +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 b42edde2f55ca2..4d4031bb428baf 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 377ddab7b877ce..40cbf0e4caa1d9 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 11c8fb9c00ef15..9ff25ce674d3da 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 20a45c586074a5..1f79104b183ee6 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/vis_default_editor/public/components/agg_group.tsx b/src/plugins/vis_default_editor/public/components/agg_group.tsx index 3030601236687d..4cde33b8fbc314 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 45abbf8d2b2dd3..39abddb3de853b 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 361eeba9abdbf8..fc79ba703c2b4a 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 c3bc72497007ea..80d53021b7866d 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 5a8cc3004a3154..023489c6d2e876 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 b3e35dac3711fb..c20a104736291b 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 c42962ad50a4b0..ec90fbd1746a15 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 ced7a38568ffd0..bd3d02029cb23a 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 52242ad11e8f58..8aeeb4ec533abc 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 a58c15f136431e..702581828e60d0 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 a94fd3f3945ab7..6e9190229114b5 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 a68bc5893406f5..1e81dbdde3f685 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 26fdd665192a62..2f9cda32fccdc9 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 321f96180fd68e..14c2a9c50ab0eb 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 bc80d549c81e6f..f6d27b54c7c640 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 e74cd180185ab3..88e6b3a29052e2 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/visualize/_line_chart.js b/test/functional/apps/visualize/_line_chart.js index 5c510617fbb017..a492f3858b524f 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/settings_page.ts b/test/functional/page_objects/settings_page.ts index fb40b946d7fa30..4b80647c8749dd 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/scripts/jenkins_security_solution_cypress.sh b/test/scripts/jenkins_security_solution_cypress.sh index 8aa3425be0beb2..204911a3eedaa6 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/x-pack/plugins/apm/common/agent_name.ts b/x-pack/plugins/apm/common/agent_name.ts index 630f5739806af4..9d462dad87ec07 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/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index 69699b72a96df7..f612ac0d383eff 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 776f74a1699668..df72fa604e4b32 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 c6b34c8b766989..7d48cee49b104e 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 814cf977c95699..805d19e2321d52 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 34347f3f959477..328b873ef85620 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 cd50f3b575113d..326d4a00fd31f1 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 8f21065b0dab06..c9e475ef15316a 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 4da7b59ec7fa52..2784d9bfd8efa8 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 00000000000000..e12a4a4831e17b --- /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 607cc2fb82f8e9..9f72ac6d5916e8 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 0e495391c94f2a..d24cb29eaf24f9 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 1ee8d79ee99a5a..a14d827eeaec5f 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 4614e06cbd45d9..589199221d7a9e 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 e5020202b77213..00000000000000 --- 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 c41dff79a916ab..2dd8ed01082fd7 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 e78a3c1cec24a5..0d1a4274c16dc9 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 7a3d9d94dec8e6..9f2483ab8a24eb 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/grokdebugger/public/index.js b/x-pack/plugins/grokdebugger/public/index.js index 960c9d8d58e4a6..d97410a2fe355f 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 6ac600c9dc97b8..c83eb85ce4d753 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/snapshot_api.ts b/x-pack/plugins/infra/common/http_api/snapshot_api.ts index 1c7dfed82783af..9ddbcb17089f3c 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 5f667beebd83b0..c12137f7810d40 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 02cef192b59efc..fa7dd62c0b8f72 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 a786283a100a98..59c24eb733f9ef 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 21379ebb1e6045..2a9f2ad13d946c 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 8f2336d11e42b2..8c9d6f393b6dde 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 538af4f5119b42..b0bfbd6693e556 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 08949ed53eb10a..2a885136f4ee71 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 961e0248c79da9..70623175f8c000 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/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts index 35d83440812d59..2c6432c3e52862 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/public/lib/lib.ts b/x-pack/plugins/infra/public/lib/lib.ts index 93f7ef644f7952..782f6ce5e0eb58 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/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 3884ee5b7279ab..fddd92128708a4 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 db5949f916ff47..723e8e581cdaa5 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 764eeb154d346a..1d94ab2f2f410c 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/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 00000000000000..b8cdc0acac1dc0 --- /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 00000000000000..d2c30a4f38ee95 --- /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 eda74da708c8f8..11f27f6401a310 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 e7bee82a9f0fee..cc177b895ca50a 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" >