diff --git a/packages/eslint-config-kibana/typescript.js b/packages/eslint-config-kibana/typescript.js index 270614ed84b69..a55ca9391011d 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/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 0fe3c1f083cf0..1b894bc400f08 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -185,27 +185,27 @@ export class ChromeService { /> ), }); + } - if (isIE()) { - notifications.toasts.addWarning({ - title: mountReactNode( - - - - ), - }} - /> - ), - }); - } + if (isIE()) { + notifications.toasts.addWarning({ + title: mountReactNode( + + + + ), + }} + /> + ), + }); } return { diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index c277dc85e5e04..46fd2b00c2304 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -51,7 +51,7 @@ const logger = loggingSystemMock.create(); expect.addSnapshotSerializer(createAbsolutePathSerializer()); -['path-1', 'path-2', 'path-3', 'path-4', 'path-5'].forEach((path) => { +['path-1', 'path-2', 'path-3', 'path-4', 'path-5', 'path-6', 'path-7', 'path-8'].forEach((path) => { jest.doMock(join(path, 'server'), () => ({}), { virtual: true, }); @@ -227,6 +227,26 @@ describe('PluginsService', () => { path: 'path-4', configPath: 'path-4-disabled', }), + createPlugin('plugin-with-disabled-optional-dep', { + path: 'path-5', + configPath: 'path-5', + optionalPlugins: ['explicitly-disabled-plugin'], + }), + createPlugin('plugin-with-missing-optional-dep', { + path: 'path-6', + configPath: 'path-6', + optionalPlugins: ['missing-plugin'], + }), + createPlugin('plugin-with-disabled-nested-transitive-dep', { + path: 'path-7', + configPath: 'path-7', + requiredPlugins: ['plugin-with-disabled-transitive-dep'], + }), + createPlugin('plugin-with-missing-nested-dep', { + path: 'path-8', + configPath: 'path-8', + requiredPlugins: ['plugin-with-missing-required-deps'], + }), ]), }); @@ -234,7 +254,7 @@ describe('PluginsService', () => { const setup = await pluginsService.setup(setupDeps); expect(setup.contracts).toBeInstanceOf(Map); - expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps); @@ -244,14 +264,20 @@ describe('PluginsService', () => { "Plugin \\"explicitly-disabled-plugin\\" is disabled.", ], Array [ - "Plugin \\"plugin-with-missing-required-deps\\" has been disabled since some of its direct or transitive dependencies are missing or disabled.", + "Plugin \\"plugin-with-missing-required-deps\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [missing-plugin]", ], Array [ - "Plugin \\"plugin-with-disabled-transitive-dep\\" has been disabled since some of its direct or transitive dependencies are missing or disabled.", + "Plugin \\"plugin-with-disabled-transitive-dep\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [another-explicitly-disabled-plugin]", ], Array [ "Plugin \\"another-explicitly-disabled-plugin\\" is disabled.", ], + Array [ + "Plugin \\"plugin-with-disabled-nested-transitive-dep\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [plugin-with-disabled-transitive-dep]", + ], + Array [ + "Plugin \\"plugin-with-missing-nested-dep\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [plugin-with-missing-required-deps]", + ], ] `); }); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 7441e753efa6a..5d1261e697bc0 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -239,11 +239,15 @@ export class PluginsService implements CoreService, parents: PluginName[] = [] - ): boolean { + ): { enabled: true } | { enabled: false; missingDependencies: string[] } { const pluginInfo = pluginEnableStatuses.get(pluginName); - return ( - pluginInfo !== undefined && - pluginInfo.isEnabled && - pluginInfo.plugin.requiredPlugins - .filter((dep) => !parents.includes(dep)) - .every((dependencyName) => - this.shouldEnablePlugin(dependencyName, pluginEnableStatuses, [...parents, pluginName]) - ) - ); + + if (pluginInfo === undefined || !pluginInfo.isEnabled) { + return { + enabled: false, + missingDependencies: [], + }; + } + + const missingDependencies = pluginInfo.plugin.requiredPlugins + .filter((dep) => !parents.includes(dep)) + .filter( + (dependencyName) => + !this.shouldEnablePlugin(dependencyName, pluginEnableStatuses, [...parents, pluginName]) + .enabled + ); + + if (missingDependencies.length === 0) { + return { + enabled: true, + }; + } + + return { + enabled: false, + missingDependencies, + }; } private registerPluginStaticDirs(deps: PluginsServiceSetupDeps) { diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js index 17610702a0bc7..30e7587707d2e 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js @@ -66,10 +66,9 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../../plugins/vis_type_vega/public/services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setInjectedVarFunc } from '../../../../../../plugins/maps_legacy/public/kibana_services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceSettings } from '../../../../../../plugins/maps_legacy/public/map/service_settings'; -import { getKibanaMapFactoryProvider } from '../../../../../../plugins/maps_legacy/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaMap } from '../../../../../../plugins/maps_legacy/public/map/kibana_map'; const THRESHOLD = 0.1; const PIXEL_DIFF = 30; @@ -82,18 +81,7 @@ describe('VegaVisualizations', () => { let vegaVisualizationDependencies; let vegaVisType; - const coreSetupMock = { - notifications: { - toasts: {}, - }, - uiSettings: { - get: () => {}, - }, - injectedMetadata: { - getInjectedVar: () => {}, - }, - }; - setKibanaMapFactory(getKibanaMapFactoryProvider(coreSetupMock)); + setKibanaMapFactory((...args) => new KibanaMap(...args)); setInjectedVars({ emsTileLayerId: {}, enableExternalUrls: true, @@ -139,30 +127,6 @@ describe('VegaVisualizations', () => { beforeEach(ngMock.module('kibana')); beforeEach( ngMock.inject(() => { - setInjectedVarFunc((injectedVar) => { - switch (injectedVar) { - case 'mapConfig': - return { - emsFileApiUrl: '', - emsTileApiUrl: '', - emsLandingPageUrl: '', - }; - case 'tilemapsConfig': - return { - deprecated: { - config: { - options: { - attribution: '123', - }, - }, - }, - }; - case 'version': - return '123'; - default: - return 'not found'; - } - }); const serviceSettings = new ServiceSettings(mockMapConfig, mockMapConfig.tilemap); vegaVisualizationDependencies = { serviceSettings, diff --git a/src/plugins/console/public/plugin.ts b/src/plugins/console/public/plugin.ts index 49a232ce35cd0..851dc7a063d7b 100644 --- a/src/plugins/console/public/plugin.ts +++ b/src/plugins/console/public/plugin.ts @@ -18,17 +18,13 @@ */ import { i18n } from '@kbn/i18n'; - -import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { Plugin, CoreSetup } from 'src/core/public'; import { FeatureCatalogueCategory } from '../../home/public'; - import { AppSetupUIPluginDependencies } from './types'; export class ConsoleUIPlugin implements Plugin { - constructor() {} - - async setup( + public setup( { notifications, getStartServices }: CoreSetup, { devTools, home, usageCollection }: AppSetupUIPluginDependencies ) { @@ -53,16 +49,25 @@ export class ConsoleUIPlugin implements Plugin { + mount: async ({ element }) => { + const [core] = await getStartServices(); + + const { + injectedMetadata, + i18n: { Context: I18nContext }, + docLinks: { DOC_LINK_VERSION }, + } = core; + const { renderApp } = await import('./application'); - const [{ injectedMetadata }] = await getStartServices(); + const elasticsearchUrl = injectedMetadata.getInjectedVar( 'elasticsearchUrl', 'http://localhost:9200' ) as string; + return renderApp({ - docLinkVersion: docLinks.DOC_LINK_VERSION, - I18nContext: i18nDep.Context, + docLinkVersion: DOC_LINK_VERSION, + I18nContext, notifications, elasticsearchUrl, usageCollection, @@ -72,5 +77,5 @@ export class ConsoleUIPlugin implements Plugin 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 32295745ce217..120bbf3b68f7b 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -95,7 +95,7 @@ export class QueryStringInputUI extends Component { public inputRef: HTMLInputElement | null = null; private persistedLog: PersistedLog | undefined; - private abortController: AbortController | undefined; + private abortController?: AbortController; private services = this.props.kibana.services; private componentIsUnmounting = false; @@ -497,6 +497,7 @@ export class QueryStringInputUI extends Component { } public componentWillUnmount() { + if (this.abortController) this.abortController.abort(); this.updateSuggestions.cancel(); this.componentIsUnmounting = true; } diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index 1a9d6bf4848f4..788ec1f145e2a 100644 --- a/src/plugins/dev_tools/public/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -16,21 +16,21 @@ * specific language governing permissions and limitations * under the License. */ + +import React, { useEffect, useRef } from 'react'; +import ReactDOM from 'react-dom'; +import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom'; +import { EuiTab, EuiTabs, EuiToolTip } from '@elastic/eui'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiTab, EuiTabs, EuiToolTip } from '@elastic/eui'; -import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom'; -import * as React from 'react'; -import ReactDOM from 'react-dom'; -import { useEffect, useRef } from 'react'; -import { AppMountContext, AppMountDeprecated, ScopedHistory } from 'kibana/public'; +import { ApplicationStart, ChromeStart, ScopedHistory } from 'src/core/public'; + import { DevToolApp } from './dev_tool'; interface DevToolsWrapperProps { devTools: readonly DevToolApp[]; activeDevTool: DevToolApp; - appMountContext: AppMountContext; updateRoute: (newRoute: string) => void; } @@ -40,12 +40,7 @@ interface MountedDevToolDescriptor { unmountHandler: () => void; } -function DevToolsWrapper({ - devTools, - activeDevTool, - appMountContext, - updateRoute, -}: DevToolsWrapperProps) { +function DevToolsWrapper({ devTools, activeDevTool, updateRoute }: DevToolsWrapperProps) { const mountedTool = useRef(null); useEffect( @@ -90,6 +85,7 @@ function DevToolsWrapper({ if (mountedTool.current) { mountedTool.current.unmountHandler(); } + const params = { element, appBasePath: '', @@ -97,9 +93,9 @@ function DevToolsWrapper({ // TODO: adapt to use Core's ScopedHistory history: {} as any, }; - const unmountHandler = isAppMountDeprecated(activeDevTool.mount) - ? await activeDevTool.mount(appMountContext, params) - : await activeDevTool.mount(params); + + const unmountHandler = await activeDevTool.mount(params); + mountedTool.current = { devTool: activeDevTool, mountpoint: element, @@ -112,19 +108,20 @@ function DevToolsWrapper({ ); } -function redirectOnMissingCapabilities(appMountContext: AppMountContext) { - if (!appMountContext.core.application.capabilities.dev_tools.show) { - appMountContext.core.application.navigateToApp('home'); +function redirectOnMissingCapabilities(application: ApplicationStart) { + if (!application.capabilities.dev_tools.show) { + application.navigateToApp('home'); return true; } return false; } -function setBadge(appMountContext: AppMountContext) { - if (appMountContext.core.application.capabilities.dev_tools.save) { +function setBadge(application: ApplicationStart, chrome: ChromeStart) { + if (application.capabilities.dev_tools.save) { return; } - appMountContext.core.chrome.setBadge({ + + chrome.setBadge({ text: i18n.translate('devTools.badge.readOnly.text', { defaultMessage: 'Read only', }), @@ -135,16 +132,16 @@ function setBadge(appMountContext: AppMountContext) { }); } -function setTitle(appMountContext: AppMountContext) { - appMountContext.core.chrome.docTitle.change( +function setTitle(chrome: ChromeStart) { + chrome.docTitle.change( i18n.translate('devTools.pageTitle', { defaultMessage: 'Dev Tools', }) ); } -function setBreadcrumbs(appMountContext: AppMountContext) { - appMountContext.core.chrome.setBreadcrumbs([ +function setBreadcrumbs(chrome: ChromeStart) { + chrome.setBreadcrumbs([ { text: i18n.translate('devTools.k7BreadcrumbsDevToolsLabel', { defaultMessage: 'Dev Tools', @@ -156,16 +153,19 @@ function setBreadcrumbs(appMountContext: AppMountContext) { export function renderApp( element: HTMLElement, - appMountContext: AppMountContext, + application: ApplicationStart, + chrome: ChromeStart, history: ScopedHistory, devTools: readonly DevToolApp[] ) { - if (redirectOnMissingCapabilities(appMountContext)) { + if (redirectOnMissingCapabilities(application)) { return () => {}; } - setBadge(appMountContext); - setBreadcrumbs(appMountContext); - setTitle(appMountContext); + + setBadge(application, chrome); + setBreadcrumbs(chrome); + setTitle(chrome); + ReactDOM.render( @@ -183,7 +183,6 @@ export function renderApp( updateRoute={props.history.push} activeDevTool={devTool} devTools={devTools} - appMountContext={appMountContext} /> )} /> @@ -208,8 +207,3 @@ export function renderApp( unlisten(); }; } - -function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated { - // Mount functions with two arguments are assumed to expect deprecated `context` object. - return mount.length === 2; -} diff --git a/src/plugins/dev_tools/public/dev_tool.ts b/src/plugins/dev_tools/public/dev_tool.ts index 943cca286a722..932897cdd7861 100644 --- a/src/plugins/dev_tools/public/dev_tool.ts +++ b/src/plugins/dev_tools/public/dev_tool.ts @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { App } from 'kibana/public'; + +import { AppMount } from 'src/core/public'; /** * Descriptor for a dev tool. A dev tool works similar to an application @@ -38,7 +39,7 @@ export class DevToolApp { * This will be used as a label in the tab above the actual tool. */ public readonly title: string; - public readonly mount: App['mount']; + public readonly mount: AppMount; /** * Flag indicating to disable the tab of this dev tool. Navigating to a @@ -66,7 +67,7 @@ export class DevToolApp { constructor( id: string, title: string, - mount: App['mount'], + mount: AppMount, enableRouting: boolean, order: number, toolTipContent = '', diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index 130d07b441b83..3ee44aaa0816e 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -18,12 +18,14 @@ */ import { BehaviorSubject } from 'rxjs'; -import { AppUpdater, CoreSetup, Plugin } from 'kibana/public'; +import { Plugin, CoreSetup, AppMountParameters } from 'src/core/public'; +import { AppUpdater } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { sortBy } from 'lodash'; + +import { AppNavLinkStatus, DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { KibanaLegacySetup } from '../../kibana_legacy/public'; import { CreateDevToolArgs, DevToolApp, createDevToolApp } from './dev_tool'; -import { AppNavLinkStatus, DEFAULT_APP_CATEGORIES } from '../../../core/public'; import './index.scss'; @@ -49,8 +51,10 @@ export class DevToolsPlugin implements Plugin { return sortBy([...this.devTools.values()], 'order'); } - public setup(core: CoreSetup, { kibanaLegacy }: { kibanaLegacy: KibanaLegacySetup }) { - core.application.register({ + public setup(coreSetup: CoreSetup, { kibanaLegacy }: { kibanaLegacy: KibanaLegacySetup }) { + const { application: applicationSetup, getStartServices } = coreSetup; + + applicationSetup.register({ id: 'dev_tools', title: i18n.translate('devTools.devToolsTitle', { defaultMessage: 'Dev Tools', @@ -59,15 +63,18 @@ export class DevToolsPlugin implements Plugin { euiIconType: 'devToolsApp', order: 9001, category: DEFAULT_APP_CATEGORIES.management, - mount: async (appMountContext, params) => { - if (!this.getSortedDevTools) { - throw new Error('not started yet'); - } + mount: async (params: AppMountParameters) => { + const { element, history } = params; + element.classList.add('devAppWrapper'); + + const [core] = await getStartServices(); + const { application, chrome } = core; + const { renderApp } = await import('./application'); - params.element.classList.add('devAppWrapper'); - return renderApp(params.element, appMountContext, params.history, this.getSortedDevTools()); + return renderApp(element, application, chrome, history, this.getSortedDevTools()); }, }); + kibanaLegacy.forwardApp('dev_tools', 'dev_tools'); return { diff --git a/src/plugins/maps_legacy/public/index.ts b/src/plugins/maps_legacy/public/index.ts index cbe8b9213d577..6b9c7d1c52db9 100644 --- a/src/plugins/maps_legacy/public/index.ts +++ b/src/plugins/maps_legacy/public/index.ts @@ -18,12 +18,10 @@ */ // @ts-ignore -import { CoreSetup, PluginInitializerContext } from 'kibana/public'; +import { PluginInitializerContext } from 'kibana/public'; // @ts-ignore import { L } from './leaflet'; -// @ts-ignore -import { KibanaMap } from './map/kibana_map'; -import { bindSetupCoreAndPlugins, MapsLegacyPlugin } from './plugin'; +import { MapsLegacyPlugin } from './plugin'; // @ts-ignore import * as colorUtil from './map/color_util'; // @ts-ignore @@ -32,8 +30,6 @@ import { KibanaMapLayer } from './map/kibana_map_layer'; import { convertToGeoJson } from './map/convert_to_geojson'; // @ts-ignore import { scaleBounds, getPrecision, geoContains } from './map/decode_geo_hash'; -// @ts-ignore -import { BaseMapsVisualizationProvider } from './map/base_maps_visualization'; import { VectorLayer, FileLayerField, @@ -75,20 +71,6 @@ export { L, }; -// Due to a leaflet/leaflet-draw bug, it's not possible to consume leaflet maps w/ draw control -// through a pipeline leveraging angular. For this reason, client plugins need to -// init kibana map and the basemaps visualization directly rather than consume through -// the usual plugin interface -export function getKibanaMapFactoryProvider(core: CoreSetup) { - bindSetupCoreAndPlugins(core); - return (...args: any) => new KibanaMap(...args); -} - -export function getBaseMapsVis(core: CoreSetup, serviceSettings: IServiceSettings) { - const getKibanaMap = getKibanaMapFactoryProvider(core); - return new BaseMapsVisualizationProvider(getKibanaMap, serviceSettings); -} - export * from './common/types'; export { ORIGIN } from './common/constants/origin'; diff --git a/src/plugins/maps_legacy/public/kibana_services.js b/src/plugins/maps_legacy/public/kibana_services.js index e0a6a6e21ab00..256b5f386d5f7 100644 --- a/src/plugins/maps_legacy/public/kibana_services.js +++ b/src/plugins/maps_legacy/public/kibana_services.js @@ -25,6 +25,12 @@ let uiSettings; export const setUiSettings = (coreUiSettings) => (uiSettings = coreUiSettings); export const getUiSettings = () => uiSettings; -let getInjectedVar; -export const setInjectedVarFunc = (getInjectedVarFunc) => (getInjectedVar = getInjectedVarFunc); -export const getInjectedVarFunc = () => getInjectedVar; +let kibanaVersion; +export const setKibanaVersion = (version) => (kibanaVersion = version); +export const getKibanaVersion = () => kibanaVersion; + +let mapsLegacyConfig; +export const setMapsLegacyConfig = (config) => (mapsLegacyConfig = config); +export const getMapsLegacyConfig = () => mapsLegacyConfig; + +export const getEmsTileLayerId = () => getMapsLegacyConfig().emsTileLayerId; diff --git a/src/plugins/maps_legacy/public/map/base_maps_visualization.js b/src/plugins/maps_legacy/public/map/base_maps_visualization.js index 2d1a45beb5d87..2d78fdc246e19 100644 --- a/src/plugins/maps_legacy/public/map/base_maps_visualization.js +++ b/src/plugins/maps_legacy/public/map/base_maps_visualization.js @@ -21,7 +21,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; import { filter, first } from 'rxjs/operators'; -import { getInjectedVarFunc, getUiSettings, getToasts } from '../kibana_services'; +import { getEmsTileLayerId, getUiSettings, getToasts } from '../kibana_services'; const WMS_MINZOOM = 0; const WMS_MAXZOOM = 22; //increase this to 22. Better for WMS @@ -129,7 +129,7 @@ export function BaseMapsVisualizationProvider(getKibanaMap, mapServiceSettings) } async _updateBaseLayer() { - const emsTileLayerId = getInjectedVarFunc()('emsTileLayerId', true); + const emsTileLayerId = getEmsTileLayerId(); if (!this._kibanaMap) { return; diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index 7c2b841e4adf3..f4f88bd5807d5 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -21,22 +21,20 @@ import _ from 'lodash'; import MarkdownIt from 'markdown-it'; import { EMSClient } from '@elastic/ems-client'; import { i18n } from '@kbn/i18n'; -import { getInjectedVarFunc } from '../kibana_services'; +import { getKibanaVersion } from '../kibana_services'; import { ORIGIN } from '../common/constants/origin'; const TMS_IN_YML_ID = 'TMS in config/kibana.yml'; export class ServiceSettings { constructor(mapConfig, tilemapsConfig) { - const getInjectedVar = getInjectedVarFunc(); this._mapConfig = mapConfig; this._tilemapsConfig = tilemapsConfig; - const kbnVersion = getInjectedVar('version'); this._showZoomMessage = true; this._emsClient = new EMSClient({ language: i18n.getLocale(), - appVersion: kbnVersion, + appVersion: getKibanaVersion(), appName: 'kibana', fileApiUrl: this._mapConfig.emsFileApiUrl, tileApiUrl: this._mapConfig.emsTileApiUrl, diff --git a/src/plugins/maps_legacy/public/plugin.ts b/src/plugins/maps_legacy/public/plugin.ts index 78c2498b9ee90..6b4e06fec9ccc 100644 --- a/src/plugins/maps_legacy/public/plugin.ts +++ b/src/plugins/maps_legacy/public/plugin.ts @@ -20,13 +20,17 @@ // @ts-ignore import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; // @ts-ignore -import { setToasts, setUiSettings, setInjectedVarFunc } from './kibana_services'; +import { setToasts, setUiSettings, setKibanaVersion, setMapsLegacyConfig } from './kibana_services'; // @ts-ignore import { ServiceSettings } from './map/service_settings'; // @ts-ignore import { getPrecision, getZoomPrecision } from './map/precision'; +// @ts-ignore +import { KibanaMap } from './map/kibana_map'; import { MapsLegacyConfigType, MapsLegacyPluginSetup, MapsLegacyPluginStart } from './index'; import { ConfigSchema } from '../config'; +// @ts-ignore +import { BaseMapsVisualizationProvider } from './map/base_maps_visualization'; /** * These are the interfaces with your public contracts. You should export these @@ -34,10 +38,15 @@ import { ConfigSchema } from '../config'; * @public */ -export const bindSetupCoreAndPlugins = (core: CoreSetup) => { +export const bindSetupCoreAndPlugins = ( + core: CoreSetup, + config: MapsLegacyConfigType, + kibanaVersion: string +) => { setToasts(core.notifications.toasts); setUiSettings(core.uiSettings); - setInjectedVarFunc(core.injectedMetadata.getInjectedVar); + setKibanaVersion(kibanaVersion); + setMapsLegacyConfig(config); }; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -53,15 +62,23 @@ export class MapsLegacyPlugin implements Plugin(); + const kibanaVersion = this._initializerContext.env.packageInfo.version; + + bindSetupCoreAndPlugins(core, config, kibanaVersion); + + const serviceSettings = new ServiceSettings(config, config.tilemap); + const getKibanaMapFactoryProvider = (...args: any) => new KibanaMap(...args); + const getBaseMapsVis = () => + new BaseMapsVisualizationProvider(getKibanaMapFactoryProvider, serviceSettings); return { - serviceSettings: new ServiceSettings(config, config.tilemap), + serviceSettings, getZoomPrecision, getPrecision, config, + getKibanaMapFactoryProvider, + getBaseMapsVis, }; } diff --git a/src/plugins/region_map/public/__tests__/region_map_visualization.js b/src/plugins/region_map/public/__tests__/region_map_visualization.js index 3dcfc7c2fc6fa..0a2a18c7cef4f 100644 --- a/src/plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/plugins/region_map/public/__tests__/region_map_visualization.js @@ -52,10 +52,11 @@ import { ExprVis } from '../../../visualizations/public/expressions/vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { BaseVisType } from '../../../visualizations/public/vis_types/base_vis_type'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setInjectedVarFunc } from '../../../maps_legacy/public/kibana_services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceSettings } from '../../../maps_legacy/public/map/service_settings'; -import { getBaseMapsVis } from '../../../maps_legacy/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { BaseMapsVisualizationProvider } from '../../../maps_legacy/public/map/base_maps_visualization'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaMap } from '../../../maps_legacy/public/map/kibana_map'; const THRESHOLD = 0.45; const PIXEL_DIFF = 96; @@ -118,14 +119,6 @@ describe('RegionMapsVisualizationTests', function () { }, }, }; - setInjectedVarFunc((injectedVar) => { - switch (injectedVar) { - case 'version': - return '123'; - default: - return 'not found'; - } - }); const serviceSettings = new ServiceSettings(mapConfig, tilemapsConfig); const regionmapsConfig = { includeElasticMapsService: true, @@ -142,7 +135,10 @@ describe('RegionMapsVisualizationTests', function () { getInjectedVar: () => {}, }, }; - const BaseMapsVisualization = getBaseMapsVis(coreSetupMock, serviceSettings); + const BaseMapsVisualization = new BaseMapsVisualizationProvider( + (...args) => new KibanaMap(...args), + serviceSettings + ); dependencies = { serviceSettings, diff --git a/src/plugins/region_map/public/plugin.ts b/src/plugins/region_map/public/plugin.ts index 6b31de758a4ca..04a2ba2f23f4e 100644 --- a/src/plugins/region_map/public/plugin.ts +++ b/src/plugins/region_map/public/plugin.ts @@ -30,7 +30,7 @@ import { VisualizationsSetup } from '../../visualizations/public'; import { createRegionMapFn } from './region_map_fn'; // @ts-ignore import { createRegionMapTypeDefinition } from './region_map_type'; -import { getBaseMapsVis, IServiceSettings, MapsLegacyPluginSetup } from '../../maps_legacy/public'; +import { IServiceSettings, MapsLegacyPluginSetup } from '../../maps_legacy/public'; import { setFormatService, setNotifications, setKibanaLegacy } from './kibana_services'; import { DataPublicPluginStart } from '../../data/public'; import { RegionMapsConfigType } from './index'; @@ -94,7 +94,7 @@ export class RegionMapPlugin implements Plugin { - switch (injectedVar) { - case 'version': - return '123'; - default: - return 'not found'; - } - }); - const coreSetupMock = { - notifications: { - toasts: {}, - }, - uiSettings: {}, - injectedMetadata: { - getInjectedVar: () => {}, - }, - }; const serviceSettings = new ServiceSettings(mapConfig, tilemapsConfig); - const BaseMapsVisualization = getBaseMapsVis(coreSetupMock, serviceSettings); + const BaseMapsVisualization = new BaseMapsVisualizationProvider( + (...args) => new KibanaMap(...args), + serviceSettings + ); const uiSettings = $injector.get('config'); dependencies = { diff --git a/src/plugins/tile_map/public/plugin.ts b/src/plugins/tile_map/public/plugin.ts index 20a45c586074a..1f79104b183ee 100644 --- a/src/plugins/tile_map/public/plugin.ts +++ b/src/plugins/tile_map/public/plugin.ts @@ -32,7 +32,7 @@ import 'angular-sanitize'; import { createTileMapFn } from './tile_map_fn'; // @ts-ignore import { createTileMapTypeDefinition } from './tile_map_type'; -import { getBaseMapsVis, MapsLegacyPluginSetup } from '../../maps_legacy/public'; +import { MapsLegacyPluginSetup } from '../../maps_legacy/public'; import { DataPublicPluginStart } from '../../data/public'; import { setFormatService, setQueryService } from './services'; import { setKibanaLegacy } from './services'; @@ -85,7 +85,7 @@ export class TileMapPlugin implements Plugin = { getZoomPrecision, getPrecision, - BaseMapsVisualization: getBaseMapsVis(core, mapsLegacy.serviceSettings), + BaseMapsVisualization: mapsLegacy.getBaseMapsVis(), uiSettings: core.uiSettings, }; diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index b3e35dac3711f..c20a104736291 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -33,7 +33,7 @@ import { import { createVegaFn } from './vega_fn'; import { createVegaTypeDefinition } from './vega_type'; -import { getKibanaMapFactoryProvider, IServiceSettings } from '../../maps_legacy/public'; +import { IServiceSettings } from '../../maps_legacy/public'; import './index.scss'; import { ConfigSchema } from '../config'; @@ -77,7 +77,7 @@ export class VegaPlugin implements Plugin, void> { emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), }); setUISettings(core.uiSettings); - setKibanaMapFactory(getKibanaMapFactoryProvider(core)); + setKibanaMapFactory(mapsLegacy.getKibanaMapFactoryProvider); setMapsLegacyConfig(mapsLegacy.config); const visualizationDependencies: Readonly = { diff --git a/x-pack/plugins/apm/common/agent_name.ts b/x-pack/plugins/apm/common/agent_name.ts index 630f5739806af..9d462dad87ec0 100644 --- a/x-pack/plugins/apm/common/agent_name.ts +++ b/x-pack/plugins/apm/common/agent_name.ts @@ -30,10 +30,12 @@ export function isAgentName(agentName: string): agentName is AgentName { return AGENT_NAMES.includes(agentName as AgentName); } +export const RUM_AGENTS = ['js-base', 'rum-js']; + export function isRumAgentName( - agentName: string | undefined + agentName?: string ): agentName is 'js-base' | 'rum-js' { - return agentName === 'js-base' || agentName === 'rum-js'; + return RUM_AGENTS.includes(agentName!); } export function isJavaAgentName( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index 776f74a169966..df72fa604e4b3 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -22,11 +22,11 @@ const ClFlexGroup = styled(EuiFlexGroup)` export function ClientMetrics() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const { data, status } = useFetcher( (callApmApi) => { - if (start && end) { + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum/client-metrics', params: { @@ -35,7 +35,7 @@ export function ClientMetrics() { }); } }, - [start, end, uiFilters] + [start, end, serviceName, uiFilters] ); const STAT_STYLE = { width: '240px' }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index c6b34c8b76698..7d48cee49b104 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -27,7 +27,7 @@ export interface PercentileRange { export const PageLoadDistribution = () => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const [percentileRange, setPercentileRange] = useState({ min: null, @@ -38,7 +38,7 @@ export const PageLoadDistribution = () => { const { data, status } = useFetcher( (callApmApi) => { - if (start && end) { + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum-client/page-load-distribution', params: { @@ -57,7 +57,14 @@ export const PageLoadDistribution = () => { }); } }, - [end, start, uiFilters, percentileRange.min, percentileRange.max] + [ + end, + start, + serviceName, + uiFilters, + percentileRange.min, + percentileRange.max, + ] ); const onPercentileChange = (min: number, max: number) => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts index 814cf977c9569..805d19e2321d5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts @@ -17,13 +17,13 @@ interface Props { export const useBreakdowns = ({ percentileRange, field, value }: Props) => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const { min: minP, max: maxP } = percentileRange ?? {}; return useFetcher( (callApmApi) => { - if (start && end && field && value) { + if (start && end && serviceName && field && value) { return callApmApi({ pathname: '/api/apm/rum-client/page-load-distribution/breakdown', params: { @@ -43,6 +43,6 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => { }); } }, - [end, start, uiFilters, field, value, minP, maxP] + [end, start, serviceName, uiFilters, field, value, minP, maxP] ); }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index 34347f3f95947..328b873ef8562 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -16,13 +16,13 @@ import { BreakdownItem } from '../../../../../typings/ui_filters'; export const PageViewsTrend = () => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const [breakdowns, setBreakdowns] = useState([]); const { data, status } = useFetcher( (callApmApi) => { - if (start && end) { + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum-client/page-view-trends', params: { @@ -40,7 +40,7 @@ export const PageViewsTrend = () => { }); } }, - [end, start, uiFilters, breakdowns] + [end, start, serviceName, uiFilters, breakdowns] ); const onBreakdownChange = (values: BreakdownItem[]) => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 8f21065b0dab0..c9e475ef15316 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -4,12 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, +} from '@elastic/eui'; import React, { useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { PROJECTION } from '../../../../common/projections/typings'; import { RumDashboard } from './RumDashboard'; +import { ServiceNameFilter } from '../../shared/LocalUIFilters/ServiceNameFilter'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { RUM_AGENTS } from '../../../../common/agent_name'; export function RumOverview() { useTrackPageview({ app: 'apm', path: 'rum_overview' }); @@ -24,12 +33,42 @@ export function RumOverview() { return config; }, []); + const { + urlParams: { start, end }, + } = useUrlParams(); + + const { data } = useFetcher( + (callApmApi) => { + if (start && end) { + return callApmApi({ + pathname: '/api/apm/services', + params: { + query: { + start, + end, + uiFilters: JSON.stringify({ agentName: RUM_AGENTS }), + }, + }, + }); + } + }, + [start, end] + ); + return ( <> - + + service.serviceName) ?? [] + } + /> + + + diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx new file mode 100644 index 0000000000000..e12a4a4831e17 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { + EuiTitle, + EuiHorizontalRule, + EuiSpacer, + EuiSelect, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { history } from '../../../../utils/history'; +import { fromQuery, toQuery } from '../../Links/url_helpers'; + +interface Props { + serviceNames: string[]; +} + +const ServiceNameFilter = ({ serviceNames }: Props) => { + const { + urlParams: { serviceName }, + } = useUrlParams(); + + const options = serviceNames.map((type) => ({ + text: type, + value: type, + })); + + const updateServiceName = (serviceN: string) => { + const newLocation = { + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + serviceName: serviceN, + }), + }; + history.push(newLocation); + }; + + useEffect(() => { + if (!serviceName && serviceNames.length > 0) { + updateServiceName(serviceNames[0]); + } + }, [serviceNames, serviceName]); + + return ( + <> + +

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

+
+ + + + { + updateServiceName(event.target.value); + }} + /> + + ); +}; + +export { ServiceNameFilter }; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts index 7a3d9d94dec8e..9f2483ab8a24e 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts @@ -16,6 +16,7 @@ import { USER_AGENT_DEVICE, USER_AGENT_OS, CLIENT_GEO_COUNTRY_ISO_CODE, + SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; const filtersByName = { @@ -85,6 +86,12 @@ const filtersByName = { }), fieldName: USER_AGENT_OS, }, + serviceName: { + title: i18n.translate('xpack.apm.localFilters.titles.serviceName', { + defaultMessage: 'Service name', + }), + fieldName: SERVICE_NAME, + }, }; export type LocalUIFilterName = keyof typeof filtersByName; diff --git a/x-pack/plugins/grokdebugger/public/index.js b/x-pack/plugins/grokdebugger/public/index.js index 960c9d8d58e4a..d97410a2fe355 100644 --- a/x-pack/plugins/grokdebugger/public/index.js +++ b/x-pack/plugins/grokdebugger/public/index.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin } from './plugin'; +import { GrokDebuggerUIPlugin } from './plugin'; export function plugin(initializerContext) { - return new Plugin(initializerContext); + return new GrokDebuggerUIPlugin(initializerContext); } diff --git a/x-pack/plugins/grokdebugger/public/plugin.js b/x-pack/plugins/grokdebugger/public/plugin.js index 6ac600c9dc97b..c83eb85ce4d75 100644 --- a/x-pack/plugins/grokdebugger/public/plugin.js +++ b/x-pack/plugins/grokdebugger/public/plugin.js @@ -6,10 +6,11 @@ import { i18n } from '@kbn/i18n'; import { first } from 'rxjs/operators'; -import { registerFeature } from './register_feature'; + import { PLUGIN } from '../common/constants'; +import { registerFeature } from './register_feature'; -export class Plugin { +export class GrokDebuggerUIPlugin { setup(coreSetup, plugins) { registerFeature(plugins.home); @@ -20,7 +21,7 @@ export class Plugin { }), id: PLUGIN.ID, enableRouting: false, - async mount(context, { element }) { + async mount({ element }) { const [coreStart] = await coreSetup.getStartServices(); const license = await plugins.licensing.license$.pipe(first()).toPromise(); const { renderApp } = await import('./render_app'); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx index ff32d78a80fc0..c9a8cabdf414b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; import { DEFAULT_PANEL, DetailParams } from '.'; import { PackageInfo } from '../../../../types'; import { AssetsFacetGroup } from '../../components/assets_facet_group'; -import { Requirements } from '../../components/requirements'; import { CenterColumn, LeftColumn, RightColumn } from './layout'; import { OverviewPanel } from './overview_panel'; import { SideNavLinks } from './side_nav_links'; @@ -73,17 +72,11 @@ export function ContentPanel(props: ContentPanelProps) { type RightColumnContentProps = PackageInfo & Pick; function RightColumnContent(props: RightColumnContentProps) { - const { assets, requirement, panel } = props; + const { assets, panel } = props; switch (panel) { case 'overview': return ( - - - - - - diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index ac455120dca83..a423722d1447a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -129,7 +129,7 @@ export interface Eval { export interface RegressionEvaluateResponse { regression: { mean_squared_error: { - error: number; + value: number; }; r_squared: { value: number; @@ -410,7 +410,7 @@ export const useRefreshAnalyticsList = ( const DEFAULT_SIG_FIGS = 3; export function getValuesFromResponse(response: RegressionEvaluateResponse) { - let meanSquaredError = response?.regression?.mean_squared_error?.error; + let meanSquaredError = response?.regression?.mean_squared_error?.value; if (meanSquaredError) { meanSquaredError = Number(meanSquaredError.toPrecision(DEFAULT_SIG_FIGS)); diff --git a/x-pack/plugins/painless_lab/public/plugin.tsx b/x-pack/plugins/painless_lab/public/plugin.tsx index 1ea6991d6023e..a5e88c8eb7fde 100644 --- a/x-pack/plugins/painless_lab/public/plugin.tsx +++ b/x-pack/plugins/painless_lab/public/plugin.tsx @@ -5,10 +5,10 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { Plugin, CoreStart, CoreSetup } from 'kibana/public'; import { first } from 'rxjs/operators'; import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Plugin, CoreSetup } from 'src/core/public'; import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; @@ -27,7 +27,7 @@ const checkLicenseStatus = (license: ILicense) => { export class PainlessLabUIPlugin implements Plugin { languageService = new LanguageService(); - async setup( + public setup( { http, getStartServices, uiSettings }: CoreSetup, { devTools, home, licensing }: PluginDependencies ) { @@ -70,7 +70,7 @@ export class PainlessLabUIPlugin implements Plugin { + mount: async ({ element }) => { const [core] = await getStartServices(); const { @@ -115,9 +115,9 @@ export class PainlessLabUIPlugin implements Plugin { }; export class SearchProfilerUIPlugin implements Plugin { - constructor(ctx: PluginInitializerContext) {} - - async setup( + public setup( { http, getStartServices }: CoreSetup, { devTools, home, licensing }: AppPublicPluginDependencies ) { @@ -47,7 +45,7 @@ export class SearchProfilerUIPlugin implements Plugin { + mount: async (params) => { const [coreStart] = await getStartServices(); const { notifications, i18n: i18nDep } = coreStart; const { boot } = await import('./application/boot'); @@ -74,7 +72,7 @@ export class SearchProfilerUIPlugin implements Plugin { expect(processEvent.process.name).not.toBeNull(); }); + describe('creates events with an empty ancestry array', () => { + let tree: Tree; + beforeEach(() => { + tree = generator.generateTree({ + alwaysGenMaxChildrenPerNode: true, + ancestors: 3, + children: 3, + generations: 3, + percentTerminated: 100, + percentWithRelated: 100, + relatedEvents: 0, + relatedAlerts: 0, + ancestryArraySize: 0, + }); + tree.ancestry.delete(tree.origin.id); + }); + + it('creates all events with an empty ancestry array', () => { + for (const event of tree.allEvents) { + expect(event.process.Ext.ancestry.length).toEqual(0); + } + }); + }); + describe('creates an origin alert when no related alerts are requested', () => { let tree: Tree; beforeEach(() => { @@ -113,6 +137,7 @@ describe('data generator', () => { percentWithRelated: 100, relatedEvents: 0, relatedAlerts: 0, + ancestryArraySize: ANCESTRY_LIMIT, }); tree.ancestry.delete(tree.origin.id); }); @@ -150,6 +175,7 @@ describe('data generator', () => { { category: RelatedEventCategory.Network, count: 1 }, ], relatedAlerts, + ancestryArraySize: ANCESTRY_LIMIT, }); }); @@ -162,29 +188,46 @@ describe('data generator', () => { }; const verifyAncestry = (event: Event, genTree: Tree) => { - if (event.process.Ext.ancestry.length > 0) { - expect(event.process.parent?.entity_id).toBe(event.process.Ext.ancestry[0]); + if (event.process.Ext.ancestry!.length > 0) { + expect(event.process.parent?.entity_id).toBe(event.process.Ext.ancestry![0]); } - for (let i = 0; i < event.process.Ext.ancestry.length; i++) { - const ancestor = event.process.Ext.ancestry[i]; + for (let i = 0; i < event.process.Ext.ancestry!.length; i++) { + const ancestor = event.process.Ext.ancestry![i]; const parent = genTree.children.get(ancestor) || genTree.ancestry.get(ancestor); expect(ancestor).toBe(parent?.lifecycle[0].process.entity_id); // the next ancestor should be the grandparent - if (i + 1 < event.process.Ext.ancestry.length) { - const grandparent = event.process.Ext.ancestry[i + 1]; + if (i + 1 < event.process.Ext.ancestry!.length) { + const grandparent = event.process.Ext.ancestry![i + 1]; expect(grandparent).toBe(parent?.lifecycle[0].process.parent?.entity_id); } } }; it('has ancestry array defined', () => { - expect(tree.origin.lifecycle[0].process.Ext.ancestry.length).toBe(ANCESTRY_LIMIT); + expect(tree.origin.lifecycle[0].process.Ext.ancestry!.length).toBe(ANCESTRY_LIMIT); for (const event of tree.allEvents) { verifyAncestry(event, tree); } }); + it('creates the right number childrenLevels', () => { + let totalChildren = 0; + for (const level of tree.childrenLevels) { + totalChildren += level.size; + } + expect(totalChildren).toEqual(tree.children.size); + expect(tree.childrenLevels.length).toEqual(generations); + }); + + it('has the right nodes in both the childrenLevels and children map', () => { + for (const level of tree.childrenLevels) { + for (const node of level.values()) { + expect(tree.children.get(node.id)).toEqual(node); + } + } + }); + it('has the right related events for each node', () => { const checkRelatedEvents = (node: TreeNode) => { expect(node.relatedEvents.length).toEqual(4); @@ -290,7 +333,11 @@ describe('data generator', () => { let events: Event[]; beforeEach(() => { - events = generator.createAlertEventAncestry(3, 0, 0, 0, 0); + events = generator.createAlertEventAncestry({ + ancestors: 3, + percentTerminated: 0, + percentWithRelated: 0, + }); }); it('with n-1 process events', () => { @@ -375,7 +422,7 @@ describe('data generator', () => { const timestamp = new Date().getTime(); const root = generator.generateEvent({ timestamp }); const generations = 2; - const events = [root, ...generator.descendantsTreeGenerator(root, generations)]; + const events = [root, ...generator.descendantsTreeGenerator(root, { generations })]; const rootNode = buildResolverTree(events); const visitedEvents = countResolverEvents(rootNode, generations); expect(visitedEvents).toEqual(events.length); diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 0cc6377c8dba6..c075e1041973b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -17,6 +17,7 @@ import { EndpointStatus, } from './types'; import { factory as policyFactory } from './models/policy_config'; +import { parentEntityId } from './models/event'; export type Event = AlertEvent | EndpointEvent; /** @@ -38,6 +39,7 @@ interface EventOptions { eventCategory?: string | string[]; processName?: string; ancestry?: string[]; + ancestryArrayLimit?: number; pid?: number; parentPid?: number; extensions?: object; @@ -266,6 +268,11 @@ export interface Tree { * Map of entity_id to node */ children: Map; + /** + * An array of levels of the children, that doesn't include the origin or any ancestors + * childrenLevels[0] are the direct children of the origin node. The next level would be those children's descendants + */ + childrenLevels: Array>; /** * Map of entity_id to node */ @@ -289,12 +296,33 @@ export interface TreeOptions { percentWithRelated?: number; percentTerminated?: number; alwaysGenMaxChildrenPerNode?: boolean; + ancestryArraySize?: number; +} + +type TreeOptionDefaults = Required; + +/** + * This function provides defaults for fields that are not specified in the options + * + * @param options tree options for defining the structure of the tree + */ +export function getTreeOptionsWithDef(options?: TreeOptions): TreeOptionDefaults { + return { + ancestors: options?.ancestors ?? 3, + generations: options?.generations ?? 2, + children: options?.children ?? 2, + relatedEvents: options?.relatedEvents ?? 5, + relatedAlerts: options?.relatedAlerts ?? 3, + percentWithRelated: options?.percentWithRelated ?? 30, + percentTerminated: options?.percentTerminated ?? 100, + alwaysGenMaxChildrenPerNode: options?.alwaysGenMaxChildrenPerNode ?? false, + ancestryArraySize: options?.ancestryArraySize ?? ANCESTRY_LIMIT, + }; } export class EndpointDocGenerator { commonInfo: HostInfo; random: seedrandom.prng; - constructor(seed: string | seedrandom.prng = Math.random().toString()) { if (typeof seed === 'string') { this.random = seedrandom(seed); @@ -373,6 +401,7 @@ export class EndpointDocGenerator { * @param ts - Timestamp to put in the event * @param entityID - entityID of the originating process * @param parentEntityID - optional entityID of the parent process, if it exists + * @param ancestryArray - an array of ancestors for the generated alert */ public generateAlert( ts = new Date().getTime(), @@ -438,9 +467,7 @@ export class EndpointDocGenerator { sha256: 'fake sha256', }, Ext: { - // simulate a finite ancestry array size, the endpoint limits the ancestry array to 20 entries we'll use - // 2 so that the backend can handle that case - ancestry: ancestryArray.slice(0, ANCESTRY_LIMIT), + ancestry: ancestryArray, code_signature: [ { trusted: false, @@ -503,6 +530,10 @@ export class EndpointDocGenerator { * @param options - Allows event field values to be specified */ public generateEvent(options: EventOptions = {}): EndpointEvent { + // this will default to an empty array for the ancestry field if options.ancestry isn't included + const ancestry: string[] = + options.ancestry?.slice(0, options?.ancestryArrayLimit ?? ANCESTRY_LIMIT) ?? []; + const processName = options.processName ? options.processName : randomProcessName(); const detailRecordForEventType = options.extensions || @@ -563,7 +594,9 @@ export class EndpointDocGenerator { name: processName, // simulate a finite ancestry array size, the endpoint limits the ancestry array to 20 entries we'll use // 2 so that the backend can handle that case - Ext: { ancestry: options.ancestry?.slice(0, ANCESTRY_LIMIT) || [] }, + Ext: { + ancestry, + }, }, user: { domain: this.randomString(10), @@ -581,6 +614,7 @@ export class EndpointDocGenerator { * @returns a Tree structure that makes accessing specific events easier */ public generateTree(options: TreeOptions = {}): Tree { + const optionsWithDef = getTreeOptionsWithDef(options); const addEventToMap = (nodeMap: Map, event: Event) => { const nodeId = event.process.entity_id; // if a node already exists for the entity_id we'll use that one, otherwise let's create a new empty node @@ -604,13 +638,46 @@ export class EndpointDocGenerator { return nodeMap.set(nodeId, node); }; - const ancestry = this.createAlertEventAncestry( - options.ancestors, - options.relatedEvents, - options.relatedAlerts, - options.percentWithRelated, - options.percentTerminated - ); + const groupNodesByParent = (children: Map) => { + const nodesByParent: Map> = new Map(); + for (const node of children.values()) { + const parentID = parentEntityId(node.lifecycle[0]); + if (parentID) { + let groupedNodes = nodesByParent.get(parentID); + + if (!groupedNodes) { + groupedNodes = new Map(); + nodesByParent.set(parentID, groupedNodes); + } + groupedNodes.set(node.id, node); + } + } + + return nodesByParent; + }; + + const createLevels = ( + childrenByParent: Map>, + levels: Array>, + currentNodes: Map | undefined + ): Array> => { + if (!currentNodes || currentNodes.size === 0) { + return levels; + } + levels.push(currentNodes); + const nextLevel: Map = new Map(); + for (const node of currentNodes.values()) { + const children = childrenByParent.get(node.id); + if (children) { + for (const child of children.values()) { + nextLevel.set(child.id, child); + } + } + } + return createLevels(childrenByParent, levels, nextLevel); + }; + + const ancestry = this.createAlertEventAncestry(optionsWithDef); // create a mapping of entity_id -> {lifecycle, related events, and related alerts} const ancestryNodes: Map = ancestry.reduce(addEventToMap, new Map()); @@ -621,26 +688,18 @@ export class EndpointDocGenerator { throw Error(`could not find origin while building tree: ${alert.process.entity_id}`); } - const children = Array.from( - this.descendantsTreeGenerator( - alert, - options.generations, - options.children, - options.relatedEvents, - options.relatedAlerts, - options.percentWithRelated, - options.percentTerminated, - options.alwaysGenMaxChildrenPerNode - ) - ); + const children = Array.from(this.descendantsTreeGenerator(alert, optionsWithDef)); const childrenNodes: Map = children.reduce(addEventToMap, new Map()); + const childrenByParent = groupNodesByParent(childrenNodes); + const levels = createLevels(childrenByParent, [], childrenByParent.get(origin.id)); return { children: childrenNodes, ancestry: ancestryNodes, allEvents: [...ancestry, ...children], origin, + childrenLevels: levels, }; } @@ -658,8 +717,9 @@ export class EndpointDocGenerator { * @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children */ public *alertsGenerator(numAlerts: number, options: TreeOptions = {}) { + const opts = getTreeOptionsWithDef(options); for (let i = 0; i < numAlerts; i++) { - yield* this.fullResolverTreeGenerator(options); + yield* this.fullResolverTreeGenerator(opts); } } @@ -678,27 +738,14 @@ export class EndpointDocGenerator { * @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children */ public *fullResolverTreeGenerator(options: TreeOptions = {}) { - const ancestry = this.createAlertEventAncestry( - options.ancestors, - options.relatedEvents, - options.relatedAlerts, - options.percentWithRelated, - options.percentTerminated - ); + const opts = getTreeOptionsWithDef(options); + + const ancestry = this.createAlertEventAncestry(opts); for (let i = 0; i < ancestry.length; i++) { yield ancestry[i]; } // ancestry will always have at least 2 elements, and the last element will be the alert - yield* this.descendantsTreeGenerator( - ancestry[ancestry.length - 1], - options.generations, - options.children, - options.relatedEvents, - options.relatedAlerts, - options.percentWithRelated, - options.percentTerminated, - options.alwaysGenMaxChildrenPerNode - ); + yield* this.descendantsTreeGenerator(ancestry[ancestry.length - 1], opts); } /** @@ -710,16 +757,14 @@ export class EndpointDocGenerator { * @param pctWithRelated - percent of ancestors that will have related events and alerts * @param pctWithTerminated - percent of ancestors that will have termination events */ - public createAlertEventAncestry( - alertAncestors = 3, - relatedEventsPerNode: RelatedEventInfo[] | number = 5, - relatedAlertsPerNode: number = 3, - pctWithRelated = 30, - pctWithTerminated = 100 - ): Event[] { + public createAlertEventAncestry(options: TreeOptions = {}): Event[] { + const opts = getTreeOptionsWithDef(options); + const events = []; const startDate = new Date().getTime(); - const root = this.generateEvent({ timestamp: startDate + 1000 }); + const root = this.generateEvent({ + timestamp: startDate + 1000, + }); events.push(root); let ancestor = root; let timestamp = root['@timestamp'] + 1000; @@ -738,7 +783,7 @@ export class EndpointDocGenerator { const addRelatedEvents = (node: Event, secBeforeEvent: number, eventList: Event[]) => { for (const relatedEvent of this.relatedEventsGenerator( node, - relatedEventsPerNode, + opts.relatedEvents, secBeforeEvent )) { eventList.push(relatedEvent); @@ -747,13 +792,13 @@ export class EndpointDocGenerator { // generate related alerts for root const processDuration: number = 6 * 3600; - if (this.randomN(100) < pctWithRelated) { + if (this.randomN(100) < opts.percentWithRelated) { addRelatedEvents(ancestor, processDuration, events); - addRelatedAlerts(ancestor, relatedAlertsPerNode, processDuration, events); + addRelatedAlerts(ancestor, opts.relatedAlerts, processDuration, events); } // generate the termination event for the root - if (this.randomN(100) < pctWithTerminated) { + if (this.randomN(100) < opts.percentTerminated) { const termProcessDuration = this.randomN(1000000); // This lets termination events be up to 1 million seconds after the creation event (~11 days) events.push( this.generateEvent({ @@ -766,19 +811,20 @@ export class EndpointDocGenerator { ); } - for (let i = 0; i < alertAncestors; i++) { + for (let i = 0; i < opts.ancestors; i++) { ancestor = this.generateEvent({ timestamp, parentEntityID: ancestor.process.entity_id, // add the parent to the ancestry array - ancestry: [ancestor.process.entity_id, ...ancestor.process.Ext.ancestry], + ancestry: [ancestor.process.entity_id, ...(ancestor.process.Ext.ancestry ?? [])], + ancestryArrayLimit: opts.ancestryArraySize, parentPid: ancestor.process.pid, pid: this.randomN(5000), }); events.push(ancestor); timestamp = timestamp + 1000; - if (this.randomN(100) < pctWithTerminated) { + if (this.randomN(100) < opts.percentTerminated) { const termProcessDuration = this.randomN(1000000); // This lets termination events be up to 1 million seconds after the creation event (~11 days) events.push( this.generateEvent({ @@ -788,18 +834,19 @@ export class EndpointDocGenerator { eventCategory: 'process', eventType: 'end', ancestry: ancestor.process.Ext.ancestry, + ancestryArrayLimit: opts.ancestryArraySize, }) ); } // generate related alerts for ancestor - if (this.randomN(100) < pctWithRelated) { + if (this.randomN(100) < opts.percentWithRelated) { addRelatedEvents(ancestor, processDuration, events); - let numAlertsPerNode = relatedAlertsPerNode; + let numAlertsPerNode = opts.relatedAlerts; // if this is the last ancestor, create one less related alert so that we have a uniform amount of related alerts // for each node. The last alert at the end of this function should always be created even if the related alerts // amount is 0 - if (i === alertAncestors - 1) { + if (i === opts.ancestors - 1) { numAlertsPerNode -= 1; } addRelatedAlerts(ancestor, numAlertsPerNode, processDuration, events); @@ -827,19 +874,11 @@ export class EndpointDocGenerator { * @param percentChildrenTerminated - percent of nodes which will have process termination events * @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children */ - public *descendantsTreeGenerator( - root: Event, - generations = 2, - maxChildrenPerNode = 2, - relatedEventsPerNode: RelatedEventInfo[] | number = 3, - relatedAlertsPerNode: number = 3, - percentNodesWithRelated = 100, - percentChildrenTerminated = 100, - alwaysGenMaxChildrenPerNode = false - ) { - let maxChildren = this.randomN(maxChildrenPerNode + 1); - if (alwaysGenMaxChildrenPerNode) { - maxChildren = maxChildrenPerNode; + public *descendantsTreeGenerator(root: Event, options: TreeOptions = {}) { + const opts = getTreeOptionsWithDef(options); + let maxChildren = this.randomN(opts.children + 1); + if (opts.alwaysGenMaxChildrenPerNode) { + maxChildren = opts.children; } const rootState: NodeState = { @@ -854,7 +893,7 @@ export class EndpointDocGenerator { // If we get to a state node and it has made all the children, move back up a level if ( currentState.childrenCreated === currentState.maxChildren || - lineage.length === generations + 1 + lineage.length === opts.generations + 1 ) { lineage.pop(); // eslint-disable-next-line no-continue @@ -868,13 +907,14 @@ export class EndpointDocGenerator { parentEntityID: currentState.event.process.entity_id, ancestry: [ currentState.event.process.entity_id, - ...currentState.event.process.Ext.ancestry, + ...(currentState.event.process.Ext.ancestry ?? []), ], + ancestryArrayLimit: opts.ancestryArraySize, }); - maxChildren = this.randomN(maxChildrenPerNode + 1); - if (alwaysGenMaxChildrenPerNode) { - maxChildren = maxChildrenPerNode; + maxChildren = this.randomN(opts.children + 1); + if (opts.alwaysGenMaxChildrenPerNode) { + maxChildren = opts.children; } lineage.push({ event: child, @@ -883,7 +923,7 @@ export class EndpointDocGenerator { }); yield child; let processDuration: number = 6 * 3600; - if (this.randomN(100) < percentChildrenTerminated) { + if (this.randomN(100) < opts.percentTerminated) { processDuration = this.randomN(1000000); // This lets termination events be up to 1 million seconds after the creation event (~11 days) yield this.generateEvent({ timestamp: timestamp + processDuration * 1000, @@ -892,11 +932,12 @@ export class EndpointDocGenerator { eventCategory: 'process', eventType: 'end', ancestry: child.process.Ext.ancestry, + ancestryArrayLimit: opts.ancestryArraySize, }); } - if (this.randomN(100) < percentNodesWithRelated) { - yield* this.relatedEventsGenerator(child, relatedEventsPerNode, processDuration); - yield* this.relatedAlertsGenerator(child, relatedAlertsPerNode, processDuration); + if (this.randomN(100) < opts.percentWithRelated) { + yield* this.relatedEventsGenerator(child, opts.relatedEvents, processDuration); + yield* this.relatedAlertsGenerator(child, opts.relatedAlerts, processDuration); } } } diff --git a/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx index d52bc4b1a267d..7085894e4a51c 100644 --- a/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx @@ -69,6 +69,21 @@ describe('useLocalStorage', () => { }); }); + it('should return presence of a message', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useMessagesStorage() + ); + await waitForNextUpdate(); + const { hasMessage, addMessage, removeMessage } = result.current; + addMessage('case', 'id-1'); + addMessage('case', 'id-2'); + removeMessage('case', 'id-2'); + expect(hasMessage('case', 'id-1')).toEqual(true); + expect(hasMessage('case', 'id-2')).toEqual(false); + }); + }); + it('should clear all messages', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => diff --git a/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.tsx b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.tsx index 0c96712ad9c53..7b9c3f74a18df 100644 --- a/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.tsx @@ -12,6 +12,7 @@ export interface UseMessagesStorage { addMessage: (plugin: string, id: string) => void; removeMessage: (plugin: string, id: string) => void; clearAllMessages: (plugin: string) => void; + hasMessage: (plugin: string, id: string) => boolean; } export const useMessagesStorage = (): UseMessagesStorage => { @@ -30,6 +31,14 @@ export const useMessagesStorage = (): UseMessagesStorage => { [storage] ); + const hasMessage = useCallback( + (plugin: string, id: string): boolean => { + const pluginStorage = storage.get(`${plugin}-messages`) ?? []; + return pluginStorage.filter((val: string) => val === id).length > 0; + }, + [storage] + ); + const removeMessage = useCallback( (plugin: string, id: string) => { const pluginStorage = storage.get(`${plugin}-messages`) ?? []; @@ -48,5 +57,6 @@ export const useMessagesStorage = (): UseMessagesStorage => { addMessage, clearAllMessages, removeMessage, + hasMessage, }; }; diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index 5e80953914c97..9aa3b007511a1 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -89,14 +89,18 @@ interface UseWithSourceState { loading: boolean; } -export const useWithSource = (sourceId = 'default', indexToAdd?: string[] | null) => { +export const useWithSource = ( + sourceId = 'default', + indexToAdd?: string[] | null, + onlyCheckIndexToAdd?: boolean +) => { const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); const defaultIndex = useMemo(() => { if (indexToAdd != null && !isEmpty(indexToAdd)) { - return [...configIndex, ...indexToAdd]; + return [...(!onlyCheckIndexToAdd ? configIndex : []), ...indexToAdd]; } return configIndex; - }, [configIndex, indexToAdd]); + }, [configIndex, indexToAdd, onlyCheckIndexToAdd]); const [state, setState] = useState({ browserFields: EMPTY_BROWSER_FIELDS, diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts index 7456be1d6784d..0fad1273c7279 100644 --- a/x-pack/plugins/security_solution/public/management/common/constants.ts +++ b/x-pack/plugins/security_solution/public/management/common/constants.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { ManagementStoreGlobalNamespace, ManagementSubTab } from '../types'; +import { APP_ID } from '../../../common/constants'; +import { SecurityPageName } from '../../app/types'; // --[ ROUTING ]--------------------------------------------------------------------------- +export const MANAGEMENT_APP_ID = `${APP_ID}:${SecurityPageName.management}`; export const MANAGEMENT_ROUTING_ROOT_PATH = ''; export const MANAGEMENT_ROUTING_ENDPOINTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.endpoints})`; export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})`; diff --git a/x-pack/plugins/security_solution/public/management/components/hooks/use_management_format_url.ts b/x-pack/plugins/security_solution/public/management/components/hooks/use_management_format_url.ts new file mode 100644 index 0000000000000..ea7d929f6044f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/hooks/use_management_format_url.ts @@ -0,0 +1,18 @@ +/* + * 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 { useKibana } from '../../../common/lib/kibana'; +import { MANAGEMENT_APP_ID } from '../../common/constants'; + +/** + * Returns a full URL to the provided Management page path by using + * kibana's `getUrlForApp()` + * + * @param managementPath + */ +export const useManagementFormatUrl = (managementPath: string) => { + return `${useKibana().services.application.getUrlForApp(MANAGEMENT_APP_ID)}${managementPath}`; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_config.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_config.tsx index 1e2307c884695..ebcfd3f1bb209 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_config.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_config.tsx @@ -14,6 +14,7 @@ import { CustomConfigurePackageConfigProps, } from '../../../../../../../ingest_manager/public'; import { getPolicyDetailPath } from '../../../../common/routing'; +import { MANAGEMENT_APP_ID } from '../../../../common/constants'; /** * Exports Endpoint-specific package config instructions @@ -59,7 +60,7 @@ export const ConfigureEndpointPackageConfig = memo { endpointPackageVersion ? `/endpoint-${endpointPackageVersion}/add-integration` : '' }`, state: { - onCancelNavigateTo: ['securitySolution:management', { path: getPoliciesPath() }], + onCancelNavigateTo: [MANAGEMENT_APP_ID, { path: getPoliciesPath() }], onCancelUrl: formatUrl(getPoliciesPath()), - onSaveNavigateTo: ['securitySolution:management', { path: getPoliciesPath() }], + onSaveNavigateTo: [MANAGEMENT_APP_ID, { path: getPoliciesPath() }], }, } ); diff --git a/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx b/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx new file mode 100644 index 0000000000000..ee048f0d61212 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx @@ -0,0 +1,65 @@ +/* + * 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, { memo } from 'react'; +import { EuiCallOut, EuiButton, EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { getEndpointListPath } from '../../../management/common/routing'; +import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; +import { useManagementFormatUrl } from '../../../management/components/hooks/use_management_format_url'; +import { MANAGEMENT_APP_ID } from '../../../management/common/constants'; + +export const EndpointNotice = memo<{ onDismiss: () => void }>(({ onDismiss }) => { + const endpointsPath = getEndpointListPath({ name: 'endpointList' }); + const endpointsLink = useManagementFormatUrl(endpointsPath); + const handleGetStartedClick = useNavigateToAppEventHandler(MANAGEMENT_APP_ID, { + path: endpointsPath, + }); + + return ( + + + + + + + } + > + <> +

+ +

+ {/* eslint-disable-next-line @elastic/eui/href-or-on-click*/} + + + + + + + +
+ ); +}); +EndpointNotice.displayName = 'EndpointNotice'; diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index d6e8fb984ac0f..bf5e7f0c211b1 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -11,6 +11,10 @@ import { MemoryRouter } from 'react-router-dom'; import '../../common/mock/match_media'; import { TestProviders } from '../../common/mock'; import { useWithSource } from '../../common/containers/source'; +import { + useMessagesStorage, + UseMessagesStorage, +} from '../../common/containers/local_storage/use_messages_storage'; import { Overview } from './index'; jest.mock('../../common/lib/kibana'); @@ -24,6 +28,17 @@ jest.mock('../../common/components/search_bar', () => ({ jest.mock('../../common/components/query_bar', () => ({ QueryBar: () => null, })); +jest.mock('../../common/containers/local_storage/use_messages_storage'); + +const endpointNoticeMessage = (hasMessageValue: boolean) => { + return { + hasMessage: () => hasMessageValue, + getMessages: () => [], + addMessage: () => undefined, + removeMessage: () => undefined, + clearAllMessages: () => undefined, + }; +}; describe('Overview', () => { describe('rendering', () => { @@ -32,6 +47,9 @@ describe('Overview', () => { indicesExist: false, }); + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + const wrapper = mount( @@ -48,6 +66,10 @@ describe('Overview', () => { indicesExist: true, indexPattern: {}, }); + + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + const wrapper = mount( @@ -57,5 +79,91 @@ describe('Overview', () => { ); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); }); + + test('it DOES render the Endpoint banner when the endpoint index is NOT available AND storage is NOT set', async () => { + (useWithSource as jest.Mock).mockReturnValueOnce({ + indicesExist: true, + indexPattern: {}, + }); + + (useWithSource as jest.Mock).mockReturnValueOnce({ + indicesExist: false, + indexPattern: {}, + }); + + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(true); + }); + + test('it does NOT render the Endpoint banner when the endpoint index is NOT available but storage is set', async () => { + (useWithSource as jest.Mock).mockReturnValueOnce({ + indicesExist: true, + indexPattern: {}, + }); + + (useWithSource as jest.Mock).mockReturnValueOnce({ + indicesExist: false, + indexPattern: {}, + }); + + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); + + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); + }); + + test('it does NOT render the Endpoint banner when the endpoint index is available AND storage is set', async () => { + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); + + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); + + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); + }); + + test('it does NOT render the Endpoint banner when an index IS available but storage is NOT set', async () => { + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); + + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 53cb32a16a9de..b8b8a67024c9f 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import React from 'react'; +import React, { useState, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { StickyContainer } from 'react-sticky'; import { Query, Filter } from 'src/plugins/data/public'; @@ -26,6 +26,9 @@ import { inputsSelectors, State } from '../../common/store'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../common/store/inputs/actions'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { SecurityPageName } from '../../app/types'; +import { EndpointNotice } from '../components/endpoint_notice'; +import { useMessagesStorage } from '../../common/containers/local_storage/use_messages_storage'; +import { ENDPOINT_METADATA_INDEX } from '../../../common/constants'; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; const NO_FILTERS: Filter[] = []; @@ -39,7 +42,27 @@ const OverviewComponent: React.FC = ({ query = DEFAULT_QUERY, setAbsoluteRangeDatePicker, }) => { + const endpointMetadataIndex = useMemo(() => { + return [ENDPOINT_METADATA_INDEX]; + }, []); + const { indicesExist, indexPattern } = useWithSource(); + const { indicesExist: metadataIndexExists } = useWithSource( + 'default', + endpointMetadataIndex, + true + ); + const { addMessage, hasMessage } = useMessagesStorage(); + const hasDismissEndpointNoticeMessage: boolean = useMemo( + () => hasMessage('management', 'dismissEndpointNotice'), + [hasMessage] + ); + + const [dismissMessage, setDismissMessage] = useState(hasDismissEndpointNoticeMessage); + const dismissEndpointNotice = () => { + setDismissMessage(true); + addMessage('management', 'dismissEndpointNotice'); + }; return ( <> @@ -50,6 +73,12 @@ const OverviewComponent: React.FC = ({ + {!dismissMessage && !metadataIndexExists && ( + <> + + + + )} diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.ts b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.ts index 53fa59060550f..cfe1c741ef3f1 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.ts @@ -11,6 +11,7 @@ import fetch from 'node-fetch'; import { Client, ClientOptions } from '@elastic/elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { indexHostsAndAlerts } from '../../common/endpoint/index_data'; +import { ANCESTRY_LIMIT } from '../../common/endpoint/generate_data'; main(); @@ -122,6 +123,12 @@ async function main() { type: 'number', default: 3, }, + ancestryArraySize: { + alias: 'ancSize', + describe: 'the upper bound size of the ancestry array, 0 will mark the field as undefined', + type: 'number', + default: ANCESTRY_LIMIT, + }, generations: { alias: 'gen', describe: 'number of child generations to create', @@ -229,6 +236,7 @@ async function main() { percentWithRelated: argv.percentWithRelated, percentTerminated: argv.percentTerminated, alwaysGenMaxChildrenPerNode: argv.maxChildrenPerNode, + ancestryArraySize: argv.ancestryArraySize, } ); console.log(`Creating and indexing documents took: ${new Date().getTime() - startTime}ms`); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts index 51c9cef08a466..1d55cb7cfd735 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts @@ -34,7 +34,16 @@ describe('Children helper', () => { const root = generator.generateEvent(); it('builds the children response structure', () => { - const children = Array.from(generator.descendantsTreeGenerator(root, 3, 3, 0, 0, 0, 100, true)); + const children = Array.from( + generator.descendantsTreeGenerator(root, { + generations: 3, + children: 3, + relatedEvents: 0, + relatedAlerts: 0, + percentTerminated: 100, + alwaysGenMaxChildrenPerNode: true, + }) + ); // because we requested the generator to always return the max children, there will always be at least 2 parents const parents = findParents(children); @@ -66,7 +75,15 @@ describe('Children helper', () => { }); it('builds the children response structure twice', () => { - const children = Array.from(generator.descendantsTreeGenerator(root, 3, 3, 0, 0, 100)); + const children = Array.from( + generator.descendantsTreeGenerator(root, { + generations: 3, + children: 3, + relatedEvents: 0, + relatedAlerts: 0, + percentTerminated: 100, + }) + ); const helper = new ChildrenNodesHelper(root.process.entity_id); helper.addChildren({}, children); helper.getNodes(); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts index eb80c840783ef..21db11f3affd3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts @@ -20,7 +20,7 @@ describe('Tree', () => { // transform the generator's array of events into the format expected by the tree class const ancestorInfo: ResolverAncestry = { ancestors: generator - .createAlertEventAncestry(5, 0, 0) + .createAlertEventAncestry({ ancestors: 5, percentTerminated: 0, percentWithRelated: 0 }) .filter((event) => { return event.event.kind === 'event'; }) diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index 7fc6405aaa303..d593dcc21b590 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -51,56 +51,21 @@ exports[`MonitorList component MonitorListPagination component renders a no item } } > - - - + pageSize={10} + setPageSize={[MockFunction]} + /> `; @@ -155,118 +120,83 @@ exports[`MonitorList component MonitorListPagination component renders the pagin } } > - - - + } + pageSize={10} + setPageSize={[MockFunction]} + /> `; @@ -321,56 +251,21 @@ exports[`MonitorList component renders a no items message when no data is provid } } > - - - + pageSize={10} + setPageSize={[MockFunction]} + /> `; @@ -425,119 +320,84 @@ exports[`MonitorList component renders error list 1`] = ` } } > - - - + } + pageSize={10} + setPageSize={[MockFunction]} + /> `; @@ -592,118 +452,83 @@ exports[`MonitorList component renders loading state 1`] = ` } } > - - - + } + pageSize={10} + setPageSize={[MockFunction]} + /> `; @@ -1476,117 +1301,82 @@ exports[`MonitorList component shallow renders the monitor list 1`] = ` } } > - - - + } + pageSize={10} + setPageSize={[MockFunction]} + /> `; diff --git a/x-pack/test/functional/apps/uptime/certificates.ts b/x-pack/test/functional/apps/uptime/certificates.ts index c7ba7816e0255..ccf35a1e63e37 100644 --- a/x-pack/test/functional/apps/uptime/certificates.ts +++ b/x-pack/test/functional/apps/uptime/certificates.ts @@ -14,7 +14,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('es'); - describe('certificates', function () { + // Failing: See https://github.com/elastic/kibana/issues/70493 + describe.skip('certificates', function () { before(async () => { await makeCheck({ es, tls: true }); await uptime.goToRoot(true);