diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 9caa3900fccfdb..ec626677d09025 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -8,6 +8,7 @@ for displayed decimal values. . Go to *Management > {kib} > Advanced Settings*. . Scroll or search for the setting you want to modify. . Enter a new value for the setting. +. Click *Save changes*. [float] @@ -34,7 +35,7 @@ removes it from {kib} permanently. [float] [[kibana-general-settings]] -=== General settings +==== General [horizontal] `csv:quoteValues`:: Set this property to `true` to quote exported values. @@ -109,7 +110,7 @@ cluster alert notifications from Monitoring. [float] [[kibana-accessibility-settings]] -=== Accessibility settings +==== Accessibility [horizontal] `accessibility:disableAnimations`:: Turns off all unnecessary animations in the @@ -117,14 +118,14 @@ cluster alert notifications from Monitoring. [float] [[kibana-dashboard-settings]] -=== Dashboard settings +==== Dashboard [horizontal] `xpackDashboardMode:roles`:: The roles that belong to <>. [float] [[kibana-discover-settings]] -=== Discover settings +==== Discover [horizontal] `context:defaultSize`:: The number of surrounding entries to display in the context view. The default value is 5. @@ -150,7 +151,7 @@ working on big documents. [float] [[kibana-notification-settings]] -=== Notifications settings +==== Notifications [horizontal] `notifications:banner`:: A custom banner intended for temporary notices to all users. @@ -169,7 +170,7 @@ displays. The default value is 10000. Set this field to `Infinity` to disable wa [float] [[kibana-reporting-settings]] -=== Reporting settings +==== Reporting [horizontal] `xpackReporting:customPdfLogo`:: A custom image to use in the footer of the PDF. @@ -177,7 +178,7 @@ displays. The default value is 10000. Set this field to `Infinity` to disable wa [float] [[kibana-rollups-settings]] -=== Rollup settings +==== Rollup [horizontal] `rollups:enableIndexPatterns`:: Enables the creation of index patterns that @@ -187,7 +188,7 @@ Refresh the page to apply the changes. [float] [[kibana-search-settings]] -=== Search settings +==== Search [horizontal] `courier:batchSearches`:: **Deprecated in 7.6. Starting in 8.0, this setting will be optimized internally.** @@ -215,21 +216,21 @@ might increase the search time. This setting is off by default. Users must opt-i [float] [[kibana-siem-settings]] -=== SIEM settings +==== SIEM [horizontal] `siem:defaultAnomalyScore`:: The threshold above which Machine Learning job anomalies are displayed in the SIEM app. `siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events. -`siem:enableNewsFeed`:: Enables the security news feed on the SIEM *Overview* +`siem:enableNewsFeed`:: Enables the security news feed on the SIEM *Overview* page. -`siem:newsFeedUrl`:: The URL from which the security news feed content is +`siem:newsFeedUrl`:: The URL from which the security news feed content is retrieved. `siem:refreshIntervalDefaults`:: The default refresh interval for the SIEM time filter, in milliseconds. `siem:timeDefaults`:: The default period of time in the SIEM time filter. [float] [[kibana-timelion-settings]] -=== Timelion settings +==== Timelion [horizontal] `timelion:default_columns`:: The default number of columns to use on a Timelion sheet. @@ -252,7 +253,7 @@ this is the number of buckets to try to represent. [float] [[kibana-visualization-settings]] -=== Visualization settings +==== Visualization [horizontal] `visualization:colorMapping`:: Maps values to specified colors in visualizations. @@ -273,7 +274,7 @@ If disabled, only visualizations that are considered production-ready are availa [float] [[kibana-telemetry-settings]] -=== Usage data settings +==== Usage data Helps improve the Elastic Stack by providing usage statistics for basic features. This data will not be shared outside of Elastic. diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts index b7e5e12f46a7fa..2165878e92ff4d 100644 --- a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts +++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts @@ -44,7 +44,10 @@ export function findKibanaPlatformPlugins(scanDirs: string[], paths: string[]) { absolute: true, } ) - .map(path => readKibanaPlatformPlugin(path)); + .map(path => + // absolute paths returned from globby are using normalize or something so the path separators are `/` even on windows, Path.resolve solves this + readKibanaPlatformPlugin(Path.resolve(path)) + ); } function readKibanaPlatformPlugin(manifestPath: string): KibanaPlatformPlugin { diff --git a/packages/kbn-optimizer/src/worker/run_worker.ts b/packages/kbn-optimizer/src/worker/run_worker.ts index d6ca2aa94fb1a9..cbec4c3f44c7d8 100644 --- a/packages/kbn-optimizer/src/worker/run_worker.ts +++ b/packages/kbn-optimizer/src/worker/run_worker.ts @@ -18,7 +18,6 @@ */ import * as Rx from 'rxjs'; -import { mergeMap } from 'rxjs/operators'; import { parseBundles, parseWorkerConfig, WorkerMsg, isWorkerMsg, WorkerMsgs } from '../common'; @@ -75,33 +74,27 @@ setInterval(() => { }, 1000).unref(); Rx.defer(() => { - return Rx.of({ - workerConfig: parseWorkerConfig(process.argv[2]), - bundles: parseBundles(process.argv[3]), - }); -}) - .pipe( - mergeMap(({ workerConfig, bundles }) => { - // set BROWSERSLIST_ENV so that style/babel loaders see it before running compilers - process.env.BROWSERSLIST_ENV = workerConfig.browserslistEnv; + const workerConfig = parseWorkerConfig(process.argv[2]); + const bundles = parseBundles(process.argv[3]); - return runCompilers(workerConfig, bundles); - }) - ) - .subscribe( - msg => { - send(msg); - }, - error => { - if (isWorkerMsg(error)) { - send(error); - } else { - send(workerMsgs.error(error)); - } + // set BROWSERSLIST_ENV so that style/babel loaders see it before running compilers + process.env.BROWSERSLIST_ENV = workerConfig.browserslistEnv; - exit(1); - }, - () => { - exit(0); + return runCompilers(workerConfig, bundles); +}).subscribe( + msg => { + send(msg); + }, + error => { + if (isWorkerMsg(error)) { + send(error); + } else { + send(workerMsgs.error(error)); } - ); + + exit(1); + }, + () => { + exit(0); + } +); diff --git a/src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap b/src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap new file mode 100644 index 00000000000000..c1b7164908ed68 --- /dev/null +++ b/src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` 1`] = ` +Array [ + Object { + "category": undefined, + "disableSubUrlTracking": undefined, + "disabled": false, + "euiIconType": undefined, + "hidden": false, + "icon": undefined, + "id": "link-a", + "linkToLastSubUrl": true, + "order": 0, + "subUrlBase": "/some-custom-url", + "title": "AppA", + "tooltip": "", + "url": "/some-custom-url", + }, + Object { + "category": undefined, + "disableSubUrlTracking": true, + "disabled": false, + "euiIconType": undefined, + "hidden": false, + "icon": undefined, + "id": "link-b", + "linkToLastSubUrl": true, + "order": 0, + "subUrlBase": "/url-b", + "title": "AppB", + "tooltip": "", + "url": "/url-b", + }, + Object { + "category": undefined, + "euiIconType": undefined, + "icon": undefined, + "id": "app-a", + "linkToLastSubUrl": true, + "order": 0, + "title": "AppA", + "url": "/app/app-a", + }, + Object { + "category": undefined, + "euiIconType": undefined, + "icon": undefined, + "id": "app-b", + "linkToLastSubUrl": true, + "order": 0, + "title": "AppB", + "url": "/app/app-b", + }, +] +`; diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts index 1c6ab91a392799..44f02f0c90d4e5 100644 --- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts +++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts @@ -30,72 +30,8 @@ import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/u import { LoggerFactory } from '../../logging'; import { PackageInfo } from '../../config'; - -import { - LegacyUiExports, - LegacyNavLink, - LegacyPluginSpec, - LegacyPluginPack, - LegacyConfig, -} from '../types'; - -const REMOVE_FROM_ARRAY: LegacyNavLink[] = []; - -function getUiAppsNavLinks({ uiAppSpecs = [] }: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]) { - return uiAppSpecs.flatMap(spec => { - if (!spec) { - return REMOVE_FROM_ARRAY; - } - - const id = spec.pluginId || spec.id; - - if (!id) { - throw new Error('Every app must specify an id'); - } - - if (spec.pluginId && !pluginSpecs.some(plugin => plugin.getId() === spec.pluginId)) { - throw new Error(`Unknown plugin id "${spec.pluginId}"`); - } - - const listed = typeof spec.listed === 'boolean' ? spec.listed : true; - - if (spec.hidden || !listed) { - return REMOVE_FROM_ARRAY; - } - - return { - id, - category: spec.category, - title: spec.title, - order: typeof spec.order === 'number' ? spec.order : 0, - icon: spec.icon, - euiIconType: spec.euiIconType, - url: spec.url || `/app/${id}`, - linkToLastSubUrl: spec.linkToLastSubUrl, - }; - }); -} - -function getNavLinks(uiExports: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]) { - return (uiExports.navLinkSpecs || []) - .map(spec => ({ - id: spec.id, - category: spec.category, - title: spec.title, - order: typeof spec.order === 'number' ? spec.order : 0, - url: spec.url, - subUrlBase: spec.subUrlBase || spec.url, - disableSubUrlTracking: spec.disableSubUrlTracking, - icon: spec.icon, - euiIconType: spec.euiIconType, - linkToLastSub: 'linkToLastSubUrl' in spec ? spec.linkToLastSubUrl : false, - hidden: 'hidden' in spec ? spec.hidden : false, - disabled: 'disabled' in spec ? spec.disabled : false, - tooltip: spec.tooltip || '', - })) - .concat(getUiAppsNavLinks(uiExports, pluginSpecs)) - .sort((a, b) => a.order - b.order); -} +import { LegacyPluginSpec, LegacyPluginPack, LegacyConfig } from '../types'; +import { getNavLinks } from './get_nav_links'; export async function findLegacyPluginSpecs( settings: unknown, diff --git a/src/core/server/legacy/plugins/get_nav_links.test.ts b/src/core/server/legacy/plugins/get_nav_links.test.ts new file mode 100644 index 00000000000000..dcb19020f769e8 --- /dev/null +++ b/src/core/server/legacy/plugins/get_nav_links.test.ts @@ -0,0 +1,283 @@ +/* + * 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 { LegacyUiExports, LegacyPluginSpec, LegacyAppSpec, LegacyNavLinkSpec } from '../types'; +import { getNavLinks } from './get_nav_links'; + +const createLegacyExports = ({ + uiAppSpecs = [], + navLinkSpecs = [], +}: { + uiAppSpecs?: LegacyAppSpec[]; + navLinkSpecs?: LegacyNavLinkSpec[]; +}): LegacyUiExports => ({ + uiAppSpecs, + navLinkSpecs, + injectedVarsReplacers: [], + defaultInjectedVarProviders: [], + savedObjectMappings: [], + savedObjectSchemas: {}, + savedObjectMigrations: {}, + savedObjectValidations: {}, +}); + +const createPluginSpecs = (...ids: string[]): LegacyPluginSpec[] => + ids.map( + id => + ({ + getId: () => id, + } as LegacyPluginSpec) + ); + +describe('getNavLinks', () => { + describe('generating from uiAppSpecs', () => { + it('generates navlinks from legacy app specs', () => { + const navlinks = getNavLinks( + createLegacyExports({ + uiAppSpecs: [ + { + id: 'app-a', + title: 'AppA', + pluginId: 'pluginA', + }, + { + id: 'app-b', + title: 'AppB', + pluginId: 'pluginA', + }, + ], + }), + createPluginSpecs('pluginA') + ); + + expect(navlinks.length).toEqual(2); + expect(navlinks[0]).toEqual( + expect.objectContaining({ + id: 'app-a', + title: 'AppA', + url: '/app/app-a', + }) + ); + expect(navlinks[1]).toEqual( + expect.objectContaining({ + id: 'app-b', + title: 'AppB', + url: '/app/app-b', + }) + ); + }); + + it('uses the app id to generates the navlink id even if pluginId is specified', () => { + const navlinks = getNavLinks( + createLegacyExports({ + uiAppSpecs: [ + { + id: 'app-a', + title: 'AppA', + pluginId: 'pluginA', + }, + { + id: 'app-b', + title: 'AppB', + pluginId: 'pluginA', + }, + ], + }), + createPluginSpecs('pluginA') + ); + + expect(navlinks.length).toEqual(2); + expect(navlinks[0].id).toEqual('app-a'); + expect(navlinks[1].id).toEqual('app-b'); + }); + + it('throws if an app reference a missing plugin', () => { + expect(() => { + getNavLinks( + createLegacyExports({ + uiAppSpecs: [ + { + id: 'app-a', + title: 'AppA', + pluginId: 'notExistingPlugin', + }, + ], + }), + createPluginSpecs('pluginA') + ); + }).toThrowErrorMatchingInlineSnapshot(`"Unknown plugin id \\"notExistingPlugin\\""`); + }); + + it('uses all known properties of the navlink', () => { + const navlinks = getNavLinks( + createLegacyExports({ + uiAppSpecs: [ + { + id: 'app-a', + title: 'AppA', + category: { + label: 'My Category', + }, + order: 42, + url: '/some-custom-url', + icon: 'fa-snowflake', + euiIconType: 'euiIcon', + linkToLastSubUrl: true, + hidden: false, + }, + ], + }), + [] + ); + expect(navlinks.length).toBe(1); + expect(navlinks[0]).toEqual({ + id: 'app-a', + title: 'AppA', + category: { + label: 'My Category', + }, + order: 42, + url: '/some-custom-url', + icon: 'fa-snowflake', + euiIconType: 'euiIcon', + linkToLastSubUrl: true, + }); + }); + }); + + describe('generating from navLinkSpecs', () => { + it('generates navlinks from legacy navLink specs', () => { + const navlinks = getNavLinks( + createLegacyExports({ + navLinkSpecs: [ + { + id: 'link-a', + title: 'AppA', + url: '/some-custom-url', + }, + { + id: 'link-b', + title: 'AppB', + url: '/some-other-url', + disableSubUrlTracking: true, + }, + ], + }), + createPluginSpecs('pluginA') + ); + + expect(navlinks.length).toEqual(2); + expect(navlinks[0]).toEqual( + expect.objectContaining({ + id: 'link-a', + title: 'AppA', + url: '/some-custom-url', + hidden: false, + disabled: false, + }) + ); + expect(navlinks[1]).toEqual( + expect.objectContaining({ + id: 'link-b', + title: 'AppB', + url: '/some-other-url', + disableSubUrlTracking: true, + }) + ); + }); + + it('only uses known properties to create the navlink', () => { + const navlinks = getNavLinks( + createLegacyExports({ + navLinkSpecs: [ + { + id: 'link-a', + title: 'AppA', + category: { + label: 'My Second Cat', + }, + order: 72, + url: '/some-other-custom', + subUrlBase: '/some-other-custom/sub', + disableSubUrlTracking: true, + icon: 'fa-corn', + euiIconType: 'euiIconBis', + linkToLastSubUrl: false, + hidden: false, + tooltip: 'My other tooltip', + }, + ], + }), + [] + ); + expect(navlinks.length).toBe(1); + expect(navlinks[0]).toEqual({ + id: 'link-a', + title: 'AppA', + category: { + label: 'My Second Cat', + }, + order: 72, + url: '/some-other-custom', + subUrlBase: '/some-other-custom/sub', + disableSubUrlTracking: true, + icon: 'fa-corn', + euiIconType: 'euiIconBis', + linkToLastSubUrl: false, + hidden: false, + disabled: false, + tooltip: 'My other tooltip', + }); + }); + }); + + describe('generating from both apps and navlinks', () => { + const navlinks = getNavLinks( + createLegacyExports({ + uiAppSpecs: [ + { + id: 'app-a', + title: 'AppA', + }, + { + id: 'app-b', + title: 'AppB', + }, + ], + navLinkSpecs: [ + { + id: 'link-a', + title: 'AppA', + url: '/some-custom-url', + }, + { + id: 'link-b', + title: 'AppB', + url: '/url-b', + disableSubUrlTracking: true, + }, + ], + }), + [] + ); + + expect(navlinks.length).toBe(4); + expect(navlinks).toMatchSnapshot(); + }); +}); diff --git a/src/core/server/legacy/plugins/get_nav_links.ts b/src/core/server/legacy/plugins/get_nav_links.ts new file mode 100644 index 00000000000000..067fb204ca7f36 --- /dev/null +++ b/src/core/server/legacy/plugins/get_nav_links.ts @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + LegacyUiExports, + LegacyNavLink, + LegacyPluginSpec, + LegacyNavLinkSpec, + LegacyAppSpec, +} from '../types'; + +function legacyAppToNavLink(spec: LegacyAppSpec): LegacyNavLink { + if (!spec.id) { + throw new Error('Every app must specify an id'); + } + return { + id: spec.id, + category: spec.category, + title: spec.title ?? spec.id, + order: typeof spec.order === 'number' ? spec.order : 0, + icon: spec.icon, + euiIconType: spec.euiIconType, + url: spec.url || `/app/${spec.id}`, + linkToLastSubUrl: spec.linkToLastSubUrl ?? true, + }; +} + +function legacyLinkToNavLink(spec: LegacyNavLinkSpec): LegacyNavLink { + return { + id: spec.id, + category: spec.category, + title: spec.title, + order: typeof spec.order === 'number' ? spec.order : 0, + url: spec.url, + subUrlBase: spec.subUrlBase || spec.url, + disableSubUrlTracking: spec.disableSubUrlTracking, + icon: spec.icon, + euiIconType: spec.euiIconType, + linkToLastSubUrl: spec.linkToLastSubUrl ?? true, + hidden: spec.hidden ?? false, + disabled: spec.disabled ?? false, + tooltip: spec.tooltip ?? '', + }; +} + +function isHidden(app: LegacyAppSpec) { + return app.listed === false || app.hidden === true; +} + +export function getNavLinks(uiExports: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]) { + const navLinkSpecs = uiExports.navLinkSpecs || []; + const appSpecs = (uiExports.uiAppSpecs || []).filter( + app => app !== undefined && !isHidden(app) + ) as LegacyAppSpec[]; + + const pluginIds = (pluginSpecs || []).map(spec => spec.getId()); + appSpecs.forEach(spec => { + if (spec.pluginId && !pluginIds.includes(spec.pluginId)) { + throw new Error(`Unknown plugin id "${spec.pluginId}"`); + } + }); + + return [...navLinkSpecs.map(legacyLinkToNavLink), ...appSpecs.map(legacyAppToNavLink)].sort( + (a, b) => a.order - b.order + ); +} diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts index d51058ca561c6b..0c1a7730f92a77 100644 --- a/src/core/server/legacy/types.ts +++ b/src/core/server/legacy/types.ts @@ -131,16 +131,20 @@ export type VarsReplacer = ( * @internal * @deprecated */ -export type LegacyNavLinkSpec = Record & ChromeNavLink; +export type LegacyNavLinkSpec = Partial & { + id: string; + title: string; + url: string; +}; /** * @internal * @deprecated */ -export type LegacyAppSpec = Pick< - ChromeNavLink, - 'title' | 'order' | 'icon' | 'euiIconType' | 'url' | 'linkToLastSubUrl' | 'hidden' | 'category' -> & { pluginId?: string; id?: string; listed?: boolean }; +export type LegacyAppSpec = Partial & { + pluginId?: string; + listed?: boolean; +}; /** * @internal diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 96a93033c74086..ad1907df571fbe 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2125,11 +2125,11 @@ export const validBodyOutput: readonly ["data", "stream"]; // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:158:3 - (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:159:3 - (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:160:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:161:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:162:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts +// src/core/server/legacy/types.ts:162:3 - (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts +// src/core/server/legacy/types.ts:163:3 - (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts +// src/core/server/legacy/types.ts:164:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts +// src/core/server/legacy/types.ts:165:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts +// src/core/server/legacy/types.ts:166:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts // src/core/server/plugins/plugins_service.ts:44:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:226:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:226:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 36563ba8cbe459..ea81193c1dd0ab 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -31,11 +31,11 @@ import { registerCspCollector } from './server/lib/csp_usage_collector'; import { injectVars } from './inject_vars'; import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; +import { kbnBaseUrl } from '../../../plugins/kibana_legacy/server'; const mkdirAsync = promisify(Fs.mkdir); export default function(kibana) { - const kbnBaseUrl = '/app/kibana'; return new kibana.Plugin({ id: 'kibana', config: function(Joi) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/index.ts index d0157882689d39..5b9fb8c0b6360b 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/index.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/index.ts @@ -25,5 +25,5 @@ export { createSavedDashboardLoader } from './saved_dashboard/saved_dashboards'; // Core will be looking for this when loading our plugin in the new platform export const plugin = (context: PluginInitializerContext) => { - return new DashboardPlugin(); + return new DashboardPlugin(context); }; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts index 9c13337a71126e..cedb6fbc9b5efa 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts @@ -19,18 +19,12 @@ import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from './legacy_imports'; -import { start as data } from '../../../data/public/legacy'; -import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; import { plugin } from './index'; (async () => { - const instance = plugin({} as PluginInitializerContext); + const instance = plugin({ + env: npSetup.plugins.kibanaLegacy.env, + } as PluginInitializerContext); instance.setup(npSetup.core, npSetup.plugins); - instance.start(npStart.core, { - ...npStart.plugins, - data, - npData: npStart.plugins.data, - embeddables, - navigation: npStart.plugins.navigation, - }); + instance.start(npStart.core, npStart.plugins); })(); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index e608eb7b7f48c0..cc104c1a931d00 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -24,8 +24,9 @@ import { AppMountContext, ChromeStart, IUiSettingsClient, - LegacyCoreStart, + CoreStart, SavedObjectsClientContract, + PluginInitializerContext, } from 'kibana/public'; import { Storage } from '../../../../../../plugins/kibana_utils/public'; import { @@ -43,13 +44,14 @@ import { import { initDashboardApp } from './legacy_app'; import { IEmbeddableStart } from '../../../../../../plugins/embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../../plugins/navigation/public'; -import { DataPublicPluginStart as NpDataStart } from '../../../../../../plugins/data/public'; +import { DataPublicPluginStart } from '../../../../../../plugins/data/public'; import { SharePluginStart } from '../../../../../../plugins/share/public'; import { KibanaLegacyStart } from '../../../../../../plugins/kibana_legacy/public'; export interface RenderDeps { - core: LegacyCoreStart; - npDataStart: NpDataStart; + pluginInitializerContext: PluginInitializerContext; + core: CoreStart; + data: DataPublicPluginStart; navigation: NavigationStart; savedObjectsClient: SavedObjectsClientContract; savedDashboards: SavedObjectLoader; @@ -58,8 +60,8 @@ export interface RenderDeps { uiSettings: IUiSettingsClient; chrome: ChromeStart; addBasePath: (path: string) => string; - savedQueryService: NpDataStart['query']['savedQueries']; - embeddables: IEmbeddableStart; + savedQueryService: DataPublicPluginStart['query']['savedQueries']; + embeddable: IEmbeddableStart; localStorage: Storage; share: SharePluginStart; config: KibanaLegacyStart['config']; @@ -71,7 +73,11 @@ export const renderApp = (element: HTMLElement, appBasePath: string, deps: Rende if (!angularModuleInstance) { angularModuleInstance = createLocalAngularModule(deps.core, deps.navigation); // global routing stuff - configureAppAngularModule(angularModuleInstance, deps.core as LegacyCoreStart, true); + configureAppAngularModule( + angularModuleInstance, + { core: deps.core, env: deps.pluginInitializerContext.env }, + true + ); initDashboardApp(angularModuleInstance, deps); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx index f94acf2dc19912..c0a0693431295b 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx @@ -103,7 +103,7 @@ export function initDashboardAppDirective(app: any, deps: RenderDeps) { $route, $scope, $routeParams, - indexPatterns: deps.npDataStart.indexPatterns, + indexPatterns: deps.data.indexPatterns, kbnUrlStateStorage, history, ...deps, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index 3f9343ededd133..465203be0d34c1 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -96,6 +96,7 @@ export class DashboardAppController { }; constructor({ + pluginInitializerContext, $scope, $route, $routeParams, @@ -103,10 +104,10 @@ export class DashboardAppController { localStorage, indexPatterns, savedQueryService, - embeddables, + embeddable, share, dashboardCapabilities, - npDataStart: { query: queryService }, + data: { query: queryService }, core: { notifications, overlays, @@ -141,7 +142,7 @@ export class DashboardAppController { const dashboardStateManager = new DashboardStateManager({ savedDashboard: dash, hideWriteControls: dashboardConfig.getHideWriteControls(), - kibanaVersion: injectedMetadata.getKibanaVersion(), + kibanaVersion: pluginInitializerContext.env.packageInfo.version, kbnUrlStateStorage, history, }); @@ -186,9 +187,9 @@ export class DashboardAppController { let panelIndexPatterns: IndexPattern[] = []; Object.values(container.getChildIds()).forEach(id => { - const embeddable = container.getChild(id); - if (isErrorEmbeddable(embeddable)) return; - const embeddableIndexPatterns = (embeddable.getOutput() as any).indexPatterns; + const embeddableInstance = container.getChild(id); + if (isErrorEmbeddable(embeddableInstance)) return; + const embeddableIndexPatterns = (embeddableInstance.getOutput() as any).indexPatterns; if (!embeddableIndexPatterns) return; panelIndexPatterns.push(...embeddableIndexPatterns); }); @@ -284,7 +285,7 @@ export class DashboardAppController { let outputSubscription: Subscription | undefined; const dashboardDom = document.getElementById('dashboardViewport'); - const dashboardFactory = embeddables.getEmbeddableFactory( + const dashboardFactory = embeddable.getEmbeddableFactory( DASHBOARD_CONTAINER_TYPE ) as DashboardContainerFactory; dashboardFactory @@ -818,8 +819,8 @@ export class DashboardAppController { if (dashboardContainer && !isErrorEmbeddable(dashboardContainer)) { openAddPanelFlyout({ embeddable: dashboardContainer, - getAllFactories: embeddables.getEmbeddableFactories, - getFactory: embeddables.getEmbeddableFactory, + getAllFactories: embeddable.getEmbeddableFactories, + getFactory: embeddable.getEmbeddableFactory, notifications, overlays, SavedObjectFinder: getSavedObjectFinder(savedObjects, uiSettings), @@ -829,7 +830,7 @@ export class DashboardAppController { navActions[TopNavIds.VISUALIZE] = async () => { const type = 'visualization'; - const factory = embeddables.getEmbeddableFactory(type); + const factory = embeddable.getEmbeddableFactory(type); if (!factory) { throw new EmbeddableFactoryNotFoundError(type); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index b0f70b7a0c68f3..ce9cc85be57b27 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -99,7 +99,7 @@ export function initDashboardApp(app, deps) { // syncs `_g` portion of url with query services const { stop: stopSyncingGlobalStateWithUrl } = syncQuery( - deps.npDataStart.query, + deps.data.query, kbnUrlStateStorage ); @@ -137,36 +137,31 @@ export function initDashboardApp(app, deps) { }, resolve: { dash: function($rootScope, $route, redirectWhenMissing, kbnUrl, history) { - return ensureDefaultIndexPattern(deps.core, deps.npDataStart, $rootScope, kbnUrl).then( - () => { - const savedObjectsClient = deps.savedObjectsClient; - const title = $route.current.params.title; - if (title) { - return savedObjectsClient - .find({ - search: `"${title}"`, - search_fields: 'title', - type: 'dashboard', - }) - .then(results => { - // The search isn't an exact match, lets see if we can find a single exact match to use - const matchingDashboards = results.savedObjects.filter( - dashboard => - dashboard.attributes.title.toLowerCase() === title.toLowerCase() - ); - if (matchingDashboards.length === 1) { - history.replace(createDashboardEditUrl(matchingDashboards[0].id)); - } else { - history.replace( - `${DashboardConstants.LANDING_PAGE_PATH}?filter="${title}"` - ); - $route.reload(); - } - return new Promise(() => {}); - }); - } + return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl).then(() => { + const savedObjectsClient = deps.savedObjectsClient; + const title = $route.current.params.title; + if (title) { + return savedObjectsClient + .find({ + search: `"${title}"`, + search_fields: 'title', + type: 'dashboard', + }) + .then(results => { + // The search isn't an exact match, lets see if we can find a single exact match to use + const matchingDashboards = results.savedObjects.filter( + dashboard => dashboard.attributes.title.toLowerCase() === title.toLowerCase() + ); + if (matchingDashboards.length === 1) { + history.replace(createDashboardEditUrl(matchingDashboards[0].id)); + } else { + history.replace(`${DashboardConstants.LANDING_PAGE_PATH}?filter="${title}"`); + $route.reload(); + } + return new Promise(() => {}); + }); } - ); + }); }, }, }) @@ -177,7 +172,7 @@ export function initDashboardApp(app, deps) { requireUICapability: 'dashboard.createNew', resolve: { dash: function(redirectWhenMissing, $rootScope, kbnUrl) { - return ensureDefaultIndexPattern(deps.core, deps.npDataStart, $rootScope, kbnUrl) + return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl) .then(() => { return deps.savedDashboards.get(); }) @@ -197,7 +192,7 @@ export function initDashboardApp(app, deps) { dash: function($rootScope, $route, redirectWhenMissing, kbnUrl, history) { const id = $route.current.params.id; - return ensureDefaultIndexPattern(deps.core, deps.npDataStart, $rootScope, kbnUrl) + return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl) .then(() => { return deps.savedDashboards.get(id); }) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index 09ae49f2305fda..7d330676e79edc 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -20,19 +20,16 @@ import { BehaviorSubject } from 'rxjs'; import { App, + AppMountParameters, CoreSetup, CoreStart, - LegacyCoreStart, Plugin, + PluginInitializerContext, SavedObjectsClientContract, } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { RenderDeps } from './np_ready/application'; -import { DataStart } from '../../../data/public'; -import { - DataPublicPluginStart as NpDataStart, - DataPublicPluginSetup as NpDataSetup, -} from '../../../../../plugins/data/public'; +import { DataPublicPluginStart, DataPublicPluginSetup } from '../../../../../plugins/data/public'; import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; @@ -52,9 +49,8 @@ import { createKbnUrlTracker } from '../../../../../plugins/kibana_utils/public' import { getQueryStateContainer } from '../../../../../plugins/data/public'; export interface DashboardPluginStartDependencies { - data: DataStart; - npData: NpDataStart; - embeddables: IEmbeddableStart; + data: DataPublicPluginStart; + embeddable: IEmbeddableStart; navigation: NavigationStart; share: SharePluginStart; kibanaLegacy: KibanaLegacyStart; @@ -63,14 +59,14 @@ export interface DashboardPluginStartDependencies { export interface DashboardPluginSetupDependencies { home: HomePublicPluginSetup; kibanaLegacy: KibanaLegacySetup; - data: NpDataSetup; + data: DataPublicPluginSetup; } export class DashboardPlugin implements Plugin { private startDependencies: { - npDataStart: NpDataStart; + data: DataPublicPluginStart; savedObjectsClient: SavedObjectsClientContract; - embeddables: IEmbeddableStart; + embeddable: IEmbeddableStart; navigation: NavigationStart; share: SharePluginStart; dashboardConfig: KibanaLegacyStart['dashboardConfig']; @@ -79,12 +75,11 @@ export class DashboardPlugin implements Plugin { private appStateUpdater = new BehaviorSubject(() => ({})); private stopUrlTracking: (() => void) | undefined = undefined; - public setup( - core: CoreSetup, - { home, kibanaLegacy, data: npData }: DashboardPluginSetupDependencies - ) { + constructor(private initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup, { home, kibanaLegacy, data }: DashboardPluginSetupDependencies) { const { querySyncStateContainer, stop: stopQuerySyncStateContainer } = getQueryStateContainer( - npData.query + data.query ); const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ baseUrl: core.http.basePath.prepend('/app/kibana'), @@ -106,41 +101,43 @@ export class DashboardPlugin implements Plugin { const app: App = { id: '', title: 'Dashboards', - mount: async ({ core: contextCore }, params) => { + mount: async (params: AppMountParameters) => { + const [coreStart] = await core.getStartServices(); if (this.startDependencies === null) { throw new Error('not started yet'); } appMounted(); const { savedObjectsClient, - embeddables, + embeddable, navigation, share, - npDataStart, + data: dataStart, dashboardConfig, } = this.startDependencies; const savedDashboards = createSavedDashboardLoader({ savedObjectsClient, - indexPatterns: npDataStart.indexPatterns, - chrome: contextCore.chrome, - overlays: contextCore.overlays, + indexPatterns: dataStart.indexPatterns, + chrome: coreStart.chrome, + overlays: coreStart.overlays, }); const deps: RenderDeps = { - core: contextCore as LegacyCoreStart, + pluginInitializerContext: this.initializerContext, + core: coreStart, dashboardConfig, navigation, share, - npDataStart, + data: dataStart, savedObjectsClient, savedDashboards, - chrome: contextCore.chrome, - addBasePath: contextCore.http.basePath.prepend, - uiSettings: contextCore.uiSettings, + chrome: coreStart.chrome, + addBasePath: coreStart.http.basePath.prepend, + uiSettings: coreStart.uiSettings, config: kibanaLegacy.config, - savedQueryService: npDataStart.query.savedQueries, - embeddables, - dashboardCapabilities: contextCore.application.capabilities.dashboard, + savedQueryService: dataStart.query.savedQueries, + embeddable, + dashboardCapabilities: coreStart.application.capabilities.dashboard, localStorage: new Storage(localStorage), }; const { renderApp } = await import('./np_ready/application'); @@ -178,18 +175,17 @@ export class DashboardPlugin implements Plugin { start( { savedObjects: { client: savedObjectsClient } }: CoreStart, { - data: dataStart, - embeddables, + embeddable, navigation, - npData, + data, share, kibanaLegacy: { dashboardConfig }, }: DashboardPluginStartDependencies ) { this.startDependencies = { - npDataStart: npData, + data, savedObjectsClient, - embeddables, + embeddable, navigation, share, dashboardConfig, diff --git a/src/legacy/core_plugins/kibana/public/home/index.ts b/src/legacy/core_plugins/kibana/public/home/index.ts index 768e1a96de9354..74b6da33c65422 100644 --- a/src/legacy/core_plugins/kibana/public/home/index.ts +++ b/src/legacy/core_plugins/kibana/public/home/index.ts @@ -17,17 +17,13 @@ * under the License. */ +import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from 'ui/new_platform'; import { HomePlugin } from './plugin'; -(async () => { - const instance = new HomePlugin(); - instance.setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - metadata: npStart.core.injectedMetadata.getLegacyMetadata(), - }, - }); +const instance = new HomePlugin({ + env: npSetup.plugins.kibanaLegacy.env, +} as PluginInitializerContext); +instance.setup(npSetup.core, npSetup.plugins); - instance.start(npStart.core, npStart.plugins); -})(); +instance.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts index 57696d874cc402..6cb1531be6b5b2 100644 --- a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts @@ -21,12 +21,10 @@ import { ChromeStart, DocLinksStart, HttpStart, - LegacyNavLink, NotificationsSetup, OverlayStart, SavedObjectsClientContract, IUiSettingsClient, - UiSettingsState, } from 'kibana/public'; import { UiStatsMetricType } from '@kbn/analytics'; import { TelemetryPluginStart } from '../../../../../plugins/telemetry/public'; @@ -39,19 +37,7 @@ import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; export interface HomeKibanaServices { indexPatternService: any; - metadata: { - app: unknown; - bundleId: string; - nav: LegacyNavLink[]; - version: string; - branch: string; - buildNum: number; - buildSha: string; - basePath: string; - serverName: string; - devMode: boolean; - uiSettings: { defaults: UiSettingsState; user?: UiSettingsState | undefined }; - }; + kibanaVersion: string; getInjected: (name: string, defaultValue?: any) => unknown; chrome: ChromeStart; uiSettings: IUiSettingsClient; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/replace_template_strings.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/replace_template_strings.js index daf996444eb3c2..c7e623657bf71f 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/replace_template_strings.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/replace_template_strings.js @@ -33,7 +33,7 @@ mustacheWriter.escapedValue = function escapedValue(token, context) { }; export function replaceTemplateStrings(text, params = {}) { - const { getInjected, metadata, docLinks } = getServices(); + const { getInjected, kibanaVersion, docLinks } = getServices(); const variables = { // '{' and '}' can not be used in template since they are used as template tags. @@ -58,7 +58,7 @@ export function replaceTemplateStrings(text, params = {}) { version: docLinks.DOC_LINK_VERSION, }, kibana: { - version: metadata.version, + version: kibanaVersion, }, }, params: params, diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts index 5cc7c9c11dd2f2..75e7cc2e453be8 100644 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts @@ -17,7 +17,13 @@ * under the License. */ -import { CoreSetup, CoreStart, LegacyNavLink, Plugin, UiSettingsState } from 'kibana/public'; +import { + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from 'kibana/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { TelemetryPluginStart } from 'src/plugins/telemetry/public'; @@ -38,21 +44,6 @@ export interface HomePluginStartDependencies { } export interface HomePluginSetupDependencies { - __LEGACY: { - metadata: { - app: unknown; - bundleId: string; - nav: LegacyNavLink[]; - version: string; - branch: string; - buildNum: number; - buildSha: string; - basePath: string; - serverName: string; - devMode: boolean; - uiSettings: { defaults: UiSettingsState; user?: UiSettingsState | undefined }; - }; - }; usageCollection: UsageCollectionSetup; kibanaLegacy: KibanaLegacySetup; home: HomePublicPluginSetup; @@ -65,31 +56,26 @@ export class HomePlugin implements Plugin { private directories: readonly FeatureCatalogueEntry[] | null = null; private telemetry?: TelemetryPluginStart; - setup( - core: CoreSetup, - { - home, - kibanaLegacy, - usageCollection, - __LEGACY: { ...legacyServices }, - }: HomePluginSetupDependencies - ) { + constructor(private initializerContext: PluginInitializerContext) {} + + setup(core: CoreSetup, { home, kibanaLegacy, usageCollection }: HomePluginSetupDependencies) { kibanaLegacy.registerLegacyApp({ id: 'home', title: 'Home', - mount: async ({ core: contextCore }, params) => { + mount: async (params: AppMountParameters) => { const trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, 'Kibana_home'); + const [coreStart] = await core.getStartServices(); setServices({ - ...legacyServices, trackUiMetric, - http: contextCore.http, + kibanaVersion: this.initializerContext.env.packageInfo.version, + http: coreStart.http, toastNotifications: core.notifications.toasts, - banners: contextCore.overlays.banners, + banners: coreStart.overlays.banners, getInjected: core.injectedMetadata.getInjectedVar, - docLinks: contextCore.docLinks, + docLinks: coreStart.docLinks, savedObjectsClient: this.savedObjectsClient!, + chrome: coreStart.chrome, telemetry: this.telemetry, - chrome: contextCore.chrome, uiSettings: core.uiSettings, addBasePath: core.http.basePath.prepend, getBasePath: core.http.basePath.get, diff --git a/src/legacy/core_plugins/kibana/public/visualize/index.ts b/src/legacy/core_plugins/kibana/public/visualize/index.ts index 83b820a8e31340..c3ae39d9fde255 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/index.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/index.ts @@ -25,5 +25,5 @@ export { VisualizeConstants, createVisualizeEditUrl } from './np_ready/visualize // Core will be looking for this when loading our plugin in the new platform export const plugin = (context: PluginInitializerContext) => { - return new VisualizePlugin(); + return new VisualizePlugin(context); }; diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index 428e6cb2257104..6082fb8428ac3f 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -19,11 +19,12 @@ import { ChromeStart, - LegacyCoreStart, + CoreStart, SavedObjectsClientContract, ToastsStart, IUiSettingsClient, I18nStart, + PluginInitializerContext, } from 'kibana/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; @@ -38,11 +39,12 @@ import { Chrome } from './legacy_imports'; import { KibanaLegacyStart } from '../../../../../plugins/kibana_legacy/public'; export interface VisualizeKibanaServices { + pluginInitializerContext: PluginInitializerContext; addBasePath: (url: string) => string; chrome: ChromeStart; - core: LegacyCoreStart; + core: CoreStart; data: DataPublicPluginStart; - embeddables: IEmbeddableStart; + embeddable: IEmbeddableStart; getBasePath: () => string; indexPatterns: IndexPatternsContract; legacyChrome: Chrome; diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy.ts index 2d615e3132b01e..bc2d700f6c6a10 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy.ts @@ -19,21 +19,19 @@ import { PluginInitializerContext } from 'kibana/public'; import { legacyChrome, npSetup, npStart } from './legacy_imports'; -import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; import { start as visualizations } from '../../../visualizations/public/np_ready/public/legacy'; import { plugin } from './index'; -(() => { - const instance = plugin({} as PluginInitializerContext); - instance.setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - legacyChrome, - }, - }); - instance.start(npStart.core, { - ...npStart.plugins, - embeddables, - visualizations, - }); -})(); +const instance = plugin({ + env: npSetup.plugins.kibanaLegacy.env, +} as PluginInitializerContext); +instance.setup(npSetup.core, { + ...npSetup.plugins, + __LEGACY: { + legacyChrome, + }, +}); +instance.start(npStart.core, { + ...npStart.plugins, + visualizations, +}); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts index 44e7e9c2a74132..3d5fd6605f56b1 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts @@ -20,7 +20,7 @@ import angular, { IModule } from 'angular'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; -import { AppMountContext, LegacyCoreStart } from 'kibana/public'; +import { AppMountContext } from 'kibana/public'; import { AppStateProvider, AppState, @@ -53,7 +53,11 @@ export const renderApp = async ( if (!angularModuleInstance) { angularModuleInstance = createLocalAngularModule(deps.core, deps.navigation); // global routing stuff - configureAppAngularModule(angularModuleInstance, deps.core as LegacyCoreStart, true); + configureAppAngularModule( + angularModuleInstance, + { core: deps.core, env: deps.pluginInitializerContext.env }, + true + ); // custom routing stuff initVisualizeApp(angularModuleInstance, deps); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index 46ae45c3a5fa2b..27fb9b63843c4a 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -31,6 +31,7 @@ import { getEditBreadcrumbs } from '../breadcrumbs'; import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util'; import { FilterStateManager } from '../../../../../data/public'; import { unhashUrl } from '../../../../../../../plugins/kibana_utils/public'; +import { kbnBaseUrl } from '../../../../../../../plugins/kibana_legacy/public'; import { SavedObjectSaveModal, showSaveModal, @@ -74,7 +75,6 @@ function VisualizeAppController( kbnUrl, redirectWhenMissing, Promise, - kbnBaseUrl, getAppState, globalState ) { diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js index 18a60f7c3c10bc..502bd6e56fb1f9 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js @@ -31,7 +31,7 @@ export function initVisualizationDirective(app, deps) { link: function($scope, element) { $scope.renderFunction = async () => { if (!$scope._handler) { - $scope._handler = await deps.embeddables + $scope._handler = await deps.embeddable .getEmbeddableFactory('visualization') .createFromObject($scope.savedObj, { timeRange: $scope.timeRange, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js index b2386f83b252c6..8032152f88173a 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js @@ -36,7 +36,7 @@ export function initVisEditorDirective(app, deps) { editor.render({ core: deps.core, data: deps.data, - embeddables: deps.embeddables, + embeddable: deps.embeddable, uiState: $scope.uiState, timeRange: $scope.timeRange, filters: $scope.filters, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts index 17be5e4051b12a..524bc4b3196b7c 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts @@ -26,7 +26,7 @@ export interface EditorRenderProps { appState: AppState; core: LegacyCoreStart; data: DataPublicPluginStart; - embeddables: IEmbeddableStart; + embeddable: IEmbeddableStart; filters: Filter[]; uiState: PersistedState; timeRange: TimeRange; diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index ce93fe7c2d5783..16715677d1e207 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -20,10 +20,11 @@ import { i18n } from '@kbn/i18n'; import { + AppMountParameters, CoreSetup, CoreStart, - LegacyCoreStart, Plugin, + PluginInitializerContext, SavedObjectsClientContract, } from 'kibana/public'; @@ -45,7 +46,7 @@ import { Chrome } from './legacy_imports'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; - embeddables: IEmbeddableStart; + embeddable: IEmbeddableStart; navigation: NavigationStart; share: SharePluginStart; visualizations: VisualizationsStart; @@ -63,13 +64,15 @@ export interface VisualizePluginSetupDependencies { export class VisualizePlugin implements Plugin { private startDependencies: { data: DataPublicPluginStart; - embeddables: IEmbeddableStart; + embeddable: IEmbeddableStart; navigation: NavigationStart; savedObjectsClient: SavedObjectsClientContract; share: SharePluginStart; visualizations: VisualizationsStart; } | null = null; + constructor(private initializerContext: PluginInitializerContext) {} + public async setup( core: CoreSetup, { home, kibanaLegacy, __LEGACY, usageCollection }: VisualizePluginSetupDependencies @@ -77,14 +80,15 @@ export class VisualizePlugin implements Plugin { kibanaLegacy.registerLegacyApp({ id: 'visualize', title: 'Visualize', - mount: async ({ core: contextCore }, params) => { + mount: async (params: AppMountParameters) => { + const [coreStart] = await core.getStartServices(); if (this.startDependencies === null) { throw new Error('not started yet'); } const { savedObjectsClient, - embeddables, + embeddable, navigation, visualizations, data, @@ -93,11 +97,12 @@ export class VisualizePlugin implements Plugin { const deps: VisualizeKibanaServices = { ...__LEGACY, - addBasePath: contextCore.http.basePath.prepend, - core: contextCore as LegacyCoreStart, - chrome: contextCore.chrome, + pluginInitializerContext: this.initializerContext, + addBasePath: coreStart.http.basePath.prepend, + core: coreStart, + chrome: coreStart.chrome, data, - embeddables, + embeddable, getBasePath: core.http.basePath.get, indexPatterns: data.indexPatterns, localStorage: new Storage(localStorage), @@ -106,13 +111,13 @@ export class VisualizePlugin implements Plugin { savedVisualizations: visualizations.getSavedVisualizationsLoader(), savedQueryService: data.query.savedQueries, share, - toastNotifications: contextCore.notifications.toasts, - uiSettings: contextCore.uiSettings, + toastNotifications: coreStart.notifications.toasts, + uiSettings: coreStart.uiSettings, config: kibanaLegacy.config, - visualizeCapabilities: contextCore.application.capabilities.visualize, + visualizeCapabilities: coreStart.application.capabilities.visualize, visualizations, usageCollection, - I18nContext: contextCore.i18n.Context, + I18nContext: coreStart.i18n.Context, }; setServices(deps); @@ -137,11 +142,11 @@ export class VisualizePlugin implements Plugin { public start( core: CoreStart, - { embeddables, navigation, data, share, visualizations }: VisualizePluginStartDependencies + { embeddable, navigation, data, share, visualizations }: VisualizePluginStartDependencies ) { this.startDependencies = { data, - embeddables, + embeddable, navigation, savedObjectsClient: core.savedObjects.client, share, diff --git a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx b/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx index 48a1a6f9d21216..32ea71c0bc0052 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx @@ -30,7 +30,7 @@ import { DefaultEditorControllerState } from './default_editor_controller'; import { getInitialWidth } from './editor_size'; function DefaultEditor({ - embeddables, + embeddable, savedObj, uiState, timeRange, @@ -56,7 +56,7 @@ function DefaultEditor({ } if (!visHandler.current) { - const embeddableFactory = embeddables.getEmbeddableFactory( + const embeddableFactory = embeddable.getEmbeddableFactory( 'visualization' ) as VisualizeEmbeddableFactory; setFactory(embeddableFactory); @@ -82,7 +82,7 @@ function DefaultEditor({ } visualize(); - }, [uiState, savedObj, timeRange, filters, appState, query, factory, embeddables]); + }, [uiState, savedObj, timeRange, filters, appState, query, factory, embeddable]); useEffect(() => { return () => { diff --git a/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js b/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js index fc12a18d72823b..3ca836e23881a8 100644 --- a/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js +++ b/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js @@ -27,13 +27,6 @@ import { $setupXsrfRequestInterceptor } from '../../../../../plugins/kibana_lega import { version } from '../../../../../core/server/utils/package_json'; const xsrfHeader = 'kbn-version'; -const newPlatform = { - injectedMetadata: { - getLegacyMetadata() { - return { version }; - }, - }, -}; describe('chrome xsrf apis', function() { const sandbox = sinon.createSandbox(); @@ -45,7 +38,7 @@ describe('chrome xsrf apis', function() { describe('jQuery support', function() { it('adds a global jQuery prefilter', function() { sandbox.stub($, 'ajaxPrefilter'); - $setupXsrfRequestInterceptor(newPlatform); + $setupXsrfRequestInterceptor(version); expect($.ajaxPrefilter.callCount).to.be(1); }); @@ -54,7 +47,7 @@ describe('chrome xsrf apis', function() { beforeEach(function() { sandbox.stub($, 'ajaxPrefilter'); - $setupXsrfRequestInterceptor(newPlatform); + $setupXsrfRequestInterceptor(version); prefilter = $.ajaxPrefilter.args[0][0]; }); @@ -79,7 +72,7 @@ describe('chrome xsrf apis', function() { beforeEach(function() { sandbox.stub($, 'ajaxPrefilter'); - ngMock.module($setupXsrfRequestInterceptor(newPlatform)); + ngMock.module($setupXsrfRequestInterceptor(version)); }); beforeEach( diff --git a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js new file mode 100644 index 00000000000000..a68a2b39398640 --- /dev/null +++ b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js @@ -0,0 +1,568 @@ +/* + * 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 '../legacy_core_editor.test.mocks'; +import RowParser from '../../../../lib/row_parser'; +import { createTokenIterator } from '../../../factories'; +import $ from 'jquery'; +import { create } from '../create'; + +describe('Input', () => { + let coreEditor; + beforeEach(() => { + // Set up our document body + document.body.innerHTML = `
+
+
+
+
`; + + coreEditor = create(document.querySelector('#ConAppEditor')); + + $(coreEditor.getContainer()).show(); + }); + afterEach(() => { + $(coreEditor.getContainer()).hide(); + }); + + describe('.getLineCount', () => { + it('returns the correct line length', async () => { + await coreEditor.setValue('1\n2\n3\n4', true); + expect(coreEditor.getLineCount()).toBe(4); + }); + }); + + describe('Tokenization', () => { + function tokensAsList() { + const iter = createTokenIterator({ + editor: coreEditor, + position: { lineNumber: 1, column: 1 }, + }); + const ret = []; + let t = iter.getCurrentToken(); + const parser = new RowParser(coreEditor); + if (parser.isEmptyToken(t)) { + t = parser.nextNonEmptyToken(iter); + } + while (t) { + ret.push({ value: t.value, type: t.type }); + t = parser.nextNonEmptyToken(iter); + } + + return ret; + } + + let testCount = 0; + + function tokenTest(tokenList, prefix, data) { + if (data && typeof data !== 'string') { + data = JSON.stringify(data, null, 3); + } + if (data) { + if (prefix) { + data = prefix + '\n' + data; + } + } else { + data = prefix; + } + + test('Token test ' + testCount++ + ' prefix: ' + prefix, async function() { + await coreEditor.setValue(data, true); + const tokens = tokensAsList(); + const normTokenList = []; + for (let i = 0; i < tokenList.length; i++) { + normTokenList.push({ type: tokenList[i++], value: tokenList[i] }); + } + + expect(tokens).toEqual(normTokenList); + }); + } + + tokenTest(['method', 'GET', 'url.part', '_search'], 'GET _search'); + + tokenTest(['method', 'GET', 'url.slash', '/', 'url.part', '_search'], 'GET /_search'); + + tokenTest( + [ + 'method', + 'GET', + 'url.protocol_host', + 'http://somehost', + 'url.slash', + '/', + 'url.part', + '_search', + ], + 'GET http://somehost/_search' + ); + + tokenTest(['method', 'GET', 'url.protocol_host', 'http://somehost'], 'GET http://somehost'); + + tokenTest( + ['method', 'GET', 'url.protocol_host', 'http://somehost', 'url.slash', '/'], + 'GET http://somehost/' + ); + + tokenTest( + ['method', 'GET', 'url.protocol_host', 'http://test:user@somehost', 'url.slash', '/'], + 'GET http://test:user@somehost/' + ); + + tokenTest( + ['method', 'GET', 'url.part', '_cluster', 'url.slash', '/', 'url.part', 'nodes'], + 'GET _cluster/nodes' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.slash', + '/', + 'url.part', + '_cluster', + 'url.slash', + '/', + 'url.part', + 'nodes', + ], + 'GET /_cluster/nodes' + ); + + tokenTest( + ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', '_search'], + 'GET index/_search' + ); + + tokenTest(['method', 'GET', 'url.part', 'index'], 'GET index'); + + tokenTest( + ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', 'type'], + 'GET index/type' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.slash', + '/', + 'url.part', + 'index', + 'url.slash', + '/', + 'url.part', + 'type', + 'url.slash', + '/', + ], + 'GET /index/type/' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.part', + 'index', + 'url.slash', + '/', + 'url.part', + 'type', + 'url.slash', + '/', + 'url.part', + '_search', + ], + 'GET index/type/_search' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.part', + 'index', + 'url.slash', + '/', + 'url.part', + 'type', + 'url.slash', + '/', + 'url.part', + '_search', + 'url.questionmark', + '?', + 'url.param', + 'value', + 'url.equal', + '=', + 'url.value', + '1', + ], + 'GET index/type/_search?value=1' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.part', + 'index', + 'url.slash', + '/', + 'url.part', + 'type', + 'url.slash', + '/', + 'url.part', + '1', + ], + 'GET index/type/1' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.slash', + '/', + 'url.part', + 'index1', + 'url.comma', + ',', + 'url.part', + 'index2', + 'url.slash', + '/', + ], + 'GET /index1,index2/' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.slash', + '/', + 'url.part', + 'index1', + 'url.comma', + ',', + 'url.part', + 'index2', + 'url.slash', + '/', + 'url.part', + '_search', + ], + 'GET /index1,index2/_search' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.part', + 'index1', + 'url.comma', + ',', + 'url.part', + 'index2', + 'url.slash', + '/', + 'url.part', + '_search', + ], + 'GET index1,index2/_search' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.slash', + '/', + 'url.part', + 'index1', + 'url.comma', + ',', + 'url.part', + 'index2', + ], + 'GET /index1,index2' + ); + + tokenTest( + ['method', 'GET', 'url.part', 'index1', 'url.comma', ',', 'url.part', 'index2'], + 'GET index1,index2' + ); + + tokenTest( + ['method', 'GET', 'url.slash', '/', 'url.part', 'index1', 'url.comma', ','], + 'GET /index1,' + ); + + tokenTest( + ['method', 'PUT', 'url.slash', '/', 'url.part', 'index', 'url.slash', '/'], + 'PUT /index/' + ); + + tokenTest( + ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', '_search'], + 'GET index/_search ' + ); + + tokenTest(['method', 'PUT', 'url.slash', '/', 'url.part', 'index'], 'PUT /index'); + + tokenTest( + [ + 'method', + 'PUT', + 'url.slash', + '/', + 'url.part', + 'index1', + 'url.comma', + ',', + 'url.part', + 'index2', + 'url.slash', + '/', + 'url.part', + 'type1', + 'url.comma', + ',', + 'url.part', + 'type2', + ], + 'PUT /index1,index2/type1,type2' + ); + + tokenTest( + [ + 'method', + 'PUT', + 'url.slash', + '/', + 'url.part', + 'index1', + 'url.slash', + '/', + 'url.part', + 'type1', + 'url.comma', + ',', + 'url.part', + 'type2', + 'url.comma', + ',', + ], + 'PUT /index1/type1,type2,' + ); + + tokenTest( + [ + 'method', + 'PUT', + 'url.part', + 'index1', + 'url.comma', + ',', + 'url.part', + 'index2', + 'url.slash', + '/', + 'url.part', + 'type1', + 'url.comma', + ',', + 'url.part', + 'type2', + 'url.slash', + '/', + 'url.part', + '1234', + ], + 'PUT index1,index2/type1,type2/1234' + ); + + tokenTest( + [ + 'method', + 'POST', + 'url.part', + '_search', + 'paren.lparen', + '{', + 'variable', + '"q"', + 'punctuation.colon', + ':', + 'paren.lparen', + '{', + 'paren.rparen', + '}', + 'paren.rparen', + '}', + ], + 'POST _search\n' + '{\n' + ' "q": {}\n' + ' \n' + '}' + ); + + tokenTest( + [ + 'method', + 'POST', + 'url.part', + '_search', + 'paren.lparen', + '{', + 'variable', + '"q"', + 'punctuation.colon', + ':', + 'paren.lparen', + '{', + 'variable', + '"s"', + 'punctuation.colon', + ':', + 'paren.lparen', + '{', + 'paren.rparen', + '}', + 'paren.rparen', + '}', + 'paren.rparen', + '}', + ], + 'POST _search\n' + '{\n' + ' "q": { "s": {}}\n' + ' \n' + '}' + ); + + function statesAsList() { + const ret = []; + const maxLine = coreEditor.getLineCount(); + for (let line = 1; line <= maxLine; line++) ret.push(coreEditor.getLineState(line)); + return ret; + } + + function statesTest(statesList, prefix, data) { + if (data && typeof data !== 'string') { + data = JSON.stringify(data, null, 3); + } + if (data) { + if (prefix) { + data = prefix + '\n' + data; + } + } else { + data = prefix; + } + + test('States test ' + testCount++ + ' prefix: ' + prefix, async function() { + await coreEditor.setValue(data, true); + const modes = statesAsList(); + expect(modes).toEqual(statesList); + }); + } + + statesTest( + ['start', 'json', 'json', 'start'], + 'POST _search\n' + '{\n' + ' "query": { "match_all": {} }\n' + '}' + ); + + statesTest( + ['start', 'json', ['json', 'json'], ['json', 'json'], 'json', 'start'], + 'POST _search\n' + '{\n' + ' "query": { \n' + ' "match_all": {} \n' + ' }\n' + '}' + ); + + statesTest( + ['start', 'json', 'json', 'start'], + 'POST _search\n' + '{\n' + ' "script": { "source": "" }\n' + '}' + ); + + statesTest( + ['start', 'json', 'json', 'start'], + 'POST _search\n' + '{\n' + ' "script": ""\n' + '}' + ); + + statesTest( + ['start', 'json', ['json', 'json'], 'json', 'start'], + 'POST _search\n' + '{\n' + ' "script": {\n' + ' }\n' + '}' + ); + + statesTest( + [ + 'start', + 'json', + ['script-start', 'json', 'json', 'json'], + ['script-start', 'json', 'json', 'json'], + ['json', 'json'], + 'json', + 'start', + ], + 'POST _search\n' + + '{\n' + + ' "test": { "script": """\n' + + ' test script\n' + + ' """\n' + + ' }\n' + + '}' + ); + + statesTest( + ['start', 'json', ['script-start', 'json'], ['script-start', 'json'], 'json', 'start'], + 'POST _search\n' + '{\n' + ' "script": """\n' + ' test script\n' + ' """,\n' + '}' + ); + + statesTest( + ['start', 'json', 'json', 'start'], + 'POST _search\n' + '{\n' + ' "script": """test script""",\n' + '}' + ); + + statesTest( + ['start', 'json', ['string_literal', 'json'], ['string_literal', 'json'], 'json', 'start'], + 'POST _search\n' + '{\n' + ' "something": """\n' + ' test script\n' + ' """,\n' + '}' + ); + + statesTest( + [ + 'start', + 'json', + ['string_literal', 'json', 'json', 'json'], + ['string_literal', 'json', 'json', 'json'], + ['json', 'json'], + ['json', 'json'], + 'json', + 'start', + ], + 'POST _search\n' + + '{\n' + + ' "something": { "f" : """\n' + + ' test script\n' + + ' """,\n' + + ' "g": 1\n' + + ' }\n' + + '}' + ); + + statesTest( + ['start', 'json', 'json', 'start'], + 'POST _search\n' + '{\n' + ' "something": """test script""",\n' + '}' + ); + }); +}); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input_tokenization.test.js b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input_tokenization.test.js deleted file mode 100644 index 019b3c1d0538aa..00000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input_tokenization.test.js +++ /dev/null @@ -1,559 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import '../legacy_core_editor.test.mocks'; -import RowParser from '../../../../lib/row_parser'; -import { createTokenIterator } from '../../../factories'; -import $ from 'jquery'; -import { create } from '../create'; - -describe('Input Tokenization', () => { - let coreEditor; - beforeEach(() => { - // Set up our document body - document.body.innerHTML = `
-
-
-
-
`; - - coreEditor = create(document.querySelector('#ConAppEditor')); - - $(coreEditor.getContainer()).show(); - }); - afterEach(() => { - $(coreEditor.getContainer()).hide(); - }); - - function tokensAsList() { - const iter = createTokenIterator({ - editor: coreEditor, - position: { lineNumber: 1, column: 1 }, - }); - const ret = []; - let t = iter.getCurrentToken(); - const parser = new RowParser(coreEditor); - if (parser.isEmptyToken(t)) { - t = parser.nextNonEmptyToken(iter); - } - while (t) { - ret.push({ value: t.value, type: t.type }); - t = parser.nextNonEmptyToken(iter); - } - - return ret; - } - - let testCount = 0; - - function tokenTest(tokenList, prefix, data) { - if (data && typeof data !== 'string') { - data = JSON.stringify(data, null, 3); - } - if (data) { - if (prefix) { - data = prefix + '\n' + data; - } - } else { - data = prefix; - } - - test('Token test ' + testCount++ + ' prefix: ' + prefix, async function() { - await coreEditor.setValue(data, true); - const tokens = tokensAsList(); - const normTokenList = []; - for (let i = 0; i < tokenList.length; i++) { - normTokenList.push({ type: tokenList[i++], value: tokenList[i] }); - } - - expect(tokens).toEqual(normTokenList); - }); - } - - tokenTest(['method', 'GET', 'url.part', '_search'], 'GET _search'); - - tokenTest(['method', 'GET', 'url.slash', '/', 'url.part', '_search'], 'GET /_search'); - - tokenTest( - [ - 'method', - 'GET', - 'url.protocol_host', - 'http://somehost', - 'url.slash', - '/', - 'url.part', - '_search', - ], - 'GET http://somehost/_search' - ); - - tokenTest(['method', 'GET', 'url.protocol_host', 'http://somehost'], 'GET http://somehost'); - - tokenTest( - ['method', 'GET', 'url.protocol_host', 'http://somehost', 'url.slash', '/'], - 'GET http://somehost/' - ); - - tokenTest( - ['method', 'GET', 'url.protocol_host', 'http://test:user@somehost', 'url.slash', '/'], - 'GET http://test:user@somehost/' - ); - - tokenTest( - ['method', 'GET', 'url.part', '_cluster', 'url.slash', '/', 'url.part', 'nodes'], - 'GET _cluster/nodes' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.slash', - '/', - 'url.part', - '_cluster', - 'url.slash', - '/', - 'url.part', - 'nodes', - ], - 'GET /_cluster/nodes' - ); - - tokenTest( - ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', '_search'], - 'GET index/_search' - ); - - tokenTest(['method', 'GET', 'url.part', 'index'], 'GET index'); - - tokenTest( - ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', 'type'], - 'GET index/type' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.slash', - '/', - 'url.part', - 'index', - 'url.slash', - '/', - 'url.part', - 'type', - 'url.slash', - '/', - ], - 'GET /index/type/' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.part', - 'index', - 'url.slash', - '/', - 'url.part', - 'type', - 'url.slash', - '/', - 'url.part', - '_search', - ], - 'GET index/type/_search' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.part', - 'index', - 'url.slash', - '/', - 'url.part', - 'type', - 'url.slash', - '/', - 'url.part', - '_search', - 'url.questionmark', - '?', - 'url.param', - 'value', - 'url.equal', - '=', - 'url.value', - '1', - ], - 'GET index/type/_search?value=1' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.part', - 'index', - 'url.slash', - '/', - 'url.part', - 'type', - 'url.slash', - '/', - 'url.part', - '1', - ], - 'GET index/type/1' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.slash', - '/', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - 'url.slash', - '/', - ], - 'GET /index1,index2/' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.slash', - '/', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - 'url.slash', - '/', - 'url.part', - '_search', - ], - 'GET /index1,index2/_search' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - 'url.slash', - '/', - 'url.part', - '_search', - ], - 'GET index1,index2/_search' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.slash', - '/', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - ], - 'GET /index1,index2' - ); - - tokenTest( - ['method', 'GET', 'url.part', 'index1', 'url.comma', ',', 'url.part', 'index2'], - 'GET index1,index2' - ); - - tokenTest( - ['method', 'GET', 'url.slash', '/', 'url.part', 'index1', 'url.comma', ','], - 'GET /index1,' - ); - - tokenTest( - ['method', 'PUT', 'url.slash', '/', 'url.part', 'index', 'url.slash', '/'], - 'PUT /index/' - ); - - tokenTest( - ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', '_search'], - 'GET index/_search ' - ); - - tokenTest(['method', 'PUT', 'url.slash', '/', 'url.part', 'index'], 'PUT /index'); - - tokenTest( - [ - 'method', - 'PUT', - 'url.slash', - '/', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - 'url.slash', - '/', - 'url.part', - 'type1', - 'url.comma', - ',', - 'url.part', - 'type2', - ], - 'PUT /index1,index2/type1,type2' - ); - - tokenTest( - [ - 'method', - 'PUT', - 'url.slash', - '/', - 'url.part', - 'index1', - 'url.slash', - '/', - 'url.part', - 'type1', - 'url.comma', - ',', - 'url.part', - 'type2', - 'url.comma', - ',', - ], - 'PUT /index1/type1,type2,' - ); - - tokenTest( - [ - 'method', - 'PUT', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - 'url.slash', - '/', - 'url.part', - 'type1', - 'url.comma', - ',', - 'url.part', - 'type2', - 'url.slash', - '/', - 'url.part', - '1234', - ], - 'PUT index1,index2/type1,type2/1234' - ); - - tokenTest( - [ - 'method', - 'POST', - 'url.part', - '_search', - 'paren.lparen', - '{', - 'variable', - '"q"', - 'punctuation.colon', - ':', - 'paren.lparen', - '{', - 'paren.rparen', - '}', - 'paren.rparen', - '}', - ], - 'POST _search\n' + '{\n' + ' "q": {}\n' + ' \n' + '}' - ); - - tokenTest( - [ - 'method', - 'POST', - 'url.part', - '_search', - 'paren.lparen', - '{', - 'variable', - '"q"', - 'punctuation.colon', - ':', - 'paren.lparen', - '{', - 'variable', - '"s"', - 'punctuation.colon', - ':', - 'paren.lparen', - '{', - 'paren.rparen', - '}', - 'paren.rparen', - '}', - 'paren.rparen', - '}', - ], - 'POST _search\n' + '{\n' + ' "q": { "s": {}}\n' + ' \n' + '}' - ); - - function statesAsList() { - const ret = []; - const maxLine = coreEditor.getLineCount(); - for (let line = 1; line <= maxLine; line++) ret.push(coreEditor.getLineState(line)); - return ret; - } - - function statesTest(statesList, prefix, data) { - if (data && typeof data !== 'string') { - data = JSON.stringify(data, null, 3); - } - if (data) { - if (prefix) { - data = prefix + '\n' + data; - } - } else { - data = prefix; - } - - test('States test ' + testCount++ + ' prefix: ' + prefix, async function() { - await coreEditor.setValue(data, true); - const modes = statesAsList(); - expect(modes).toEqual(statesList); - }); - } - - statesTest( - ['start', 'json', 'json', 'start'], - 'POST _search\n' + '{\n' + ' "query": { "match_all": {} }\n' + '}' - ); - - statesTest( - ['start', 'json', ['json', 'json'], ['json', 'json'], 'json', 'start'], - 'POST _search\n' + '{\n' + ' "query": { \n' + ' "match_all": {} \n' + ' }\n' + '}' - ); - - statesTest( - ['start', 'json', 'json', 'start'], - 'POST _search\n' + '{\n' + ' "script": { "source": "" }\n' + '}' - ); - - statesTest( - ['start', 'json', 'json', 'start'], - 'POST _search\n' + '{\n' + ' "script": ""\n' + '}' - ); - - statesTest( - ['start', 'json', ['json', 'json'], 'json', 'start'], - 'POST _search\n' + '{\n' + ' "script": {\n' + ' }\n' + '}' - ); - - statesTest( - [ - 'start', - 'json', - ['script-start', 'json', 'json', 'json'], - ['script-start', 'json', 'json', 'json'], - ['json', 'json'], - 'json', - 'start', - ], - 'POST _search\n' + - '{\n' + - ' "test": { "script": """\n' + - ' test script\n' + - ' """\n' + - ' }\n' + - '}' - ); - - statesTest( - ['start', 'json', ['script-start', 'json'], ['script-start', 'json'], 'json', 'start'], - 'POST _search\n' + '{\n' + ' "script": """\n' + ' test script\n' + ' """,\n' + '}' - ); - - statesTest( - ['start', 'json', 'json', 'start'], - 'POST _search\n' + '{\n' + ' "script": """test script""",\n' + '}' - ); - - statesTest( - ['start', 'json', ['string_literal', 'json'], ['string_literal', 'json'], 'json', 'start'], - 'POST _search\n' + '{\n' + ' "something": """\n' + ' test script\n' + ' """,\n' + '}' - ); - - statesTest( - [ - 'start', - 'json', - ['string_literal', 'json', 'json', 'json'], - ['string_literal', 'json', 'json', 'json'], - ['json', 'json'], - ['json', 'json'], - 'json', - 'start', - ], - 'POST _search\n' + - '{\n' + - ' "something": { "f" : """\n' + - ' test script\n' + - ' """,\n' + - ' "g": 1\n' + - ' }\n' + - '}' - ); - - statesTest( - ['start', 'json', 'json', 'start'], - 'POST _search\n' + '{\n' + ' "something": """test script""",\n' + '}' - ); -}); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts index 19a86648d6dd34..47947e985092bc 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts @@ -189,8 +189,9 @@ export class LegacyCoreEditor implements CoreEditor { } getLineCount() { - const text = this.getValue(); - return text.split('\n').length; + // Only use this function to return line count as it uses + // a cache. + return this.editor.getSession().getLength(); } addMarker(range: Range) { diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts index 9679eaa2884ce2..1271f167c6cc11 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts @@ -78,7 +78,7 @@ export class SenseEditor { } else { curRow = rowOrPos as number; } - const maxLines = this.coreEditor.getValue().split('\n').length; + const maxLines = this.coreEditor.getLineCount(); for (; curRow < maxLines - 1; curRow++) { if (this.parser.isStartRequestRow(curRow, this.coreEditor)) { break; diff --git a/src/plugins/console/public/lib/ace_token_provider/token_provider.ts b/src/plugins/console/public/lib/ace_token_provider/token_provider.ts index 761eb1d206cfe5..134ab6c0e82d5a 100644 --- a/src/plugins/console/public/lib/ace_token_provider/token_provider.ts +++ b/src/plugins/console/public/lib/ace_token_provider/token_provider.ts @@ -66,7 +66,10 @@ export class AceTokensProvider implements TokensProvider { getTokens(lineNumber: number): Token[] | null { if (lineNumber < 1) return null; - const lineCount = this.session.doc.getAllLines().length; + // Important: must use a .session.getLength because this is a cached value. + // Calculating line length here will lead to performance issues because this function + // may be called inside of tight loops. + const lineCount = this.session.getLength(); if (lineNumber > lineCount) { return null; } diff --git a/src/plugins/console/public/types/core_editor.ts b/src/plugins/console/public/types/core_editor.ts index 8de4c78333feea..79dc3ca74200b2 100644 --- a/src/plugins/console/public/types/core_editor.ts +++ b/src/plugins/console/public/types/core_editor.ts @@ -181,6 +181,10 @@ export interface CoreEditor { /** * Return the current line count in the buffer. + * + * @remark + * This function should be usable in a tight loop and must make used of a cached + * line count. */ getLineCount(): number; diff --git a/src/plugins/kibana_legacy/common/kbn_base_url.ts b/src/plugins/kibana_legacy/common/kbn_base_url.ts new file mode 100644 index 00000000000000..69711626750ea3 --- /dev/null +++ b/src/plugins/kibana_legacy/common/kbn_base_url.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const kbnBaseUrl = '/app/kibana'; diff --git a/src/plugins/kibana_legacy/public/angular/angular_config.tsx b/src/plugins/kibana_legacy/public/angular/angular_config.tsx index 9a33cff82ed633..67d62cab7409bf 100644 --- a/src/plugins/kibana_legacy/public/angular/angular_config.tsx +++ b/src/plugins/kibana_legacy/public/angular/angular_config.tsx @@ -31,7 +31,7 @@ import $ from 'jquery'; import { cloneDeep, forOwn, get, set } from 'lodash'; import React, { Fragment } from 'react'; import * as Rx from 'rxjs'; -import { ChromeBreadcrumb } from 'kibana/public'; +import { ChromeBreadcrumb, EnvironmentMode, PackageInfo } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -79,34 +79,53 @@ function isDummyRoute($route: any, isLocalAngular: boolean) { export const configureAppAngularModule = ( angularModule: IModule, - newPlatform: LegacyCoreStart, + newPlatform: + | LegacyCoreStart + | { + core: CoreStart; + readonly env: { + mode: Readonly; + packageInfo: Readonly; + }; + }, isLocalAngular: boolean ) => { - const legacyMetadata = newPlatform.injectedMetadata.getLegacyMetadata(); - - forOwn(newPlatform.injectedMetadata.getInjectedVars(), (val, name) => { - if (name !== undefined) { - // The legacy platform modifies some of these values, clone to an unfrozen object. - angularModule.value(name, cloneDeep(val)); - } - }); + const core = 'core' in newPlatform ? newPlatform.core : newPlatform; + const packageInfo = + 'injectedMetadata' in newPlatform + ? newPlatform.injectedMetadata.getLegacyMetadata() + : newPlatform.env.packageInfo; + + if ('injectedMetadata' in newPlatform) { + forOwn(newPlatform.injectedMetadata.getInjectedVars(), (val, name) => { + if (name !== undefined) { + // The legacy platform modifies some of these values, clone to an unfrozen object. + angularModule.value(name, cloneDeep(val)); + } + }); + } angularModule - .value('kbnVersion', newPlatform.injectedMetadata.getKibanaVersion()) - .value('buildNum', legacyMetadata.buildNum) - .value('buildSha', legacyMetadata.buildSha) - .value('serverName', legacyMetadata.serverName) - .value('esUrl', getEsUrl(newPlatform)) - .value('uiCapabilities', newPlatform.application.capabilities) - .config(setupCompileProvider(newPlatform)) + .value('kbnVersion', packageInfo.version) + .value('buildNum', packageInfo.buildNum) + .value('buildSha', packageInfo.buildSha) + .value('esUrl', getEsUrl(core)) + .value('uiCapabilities', core.application.capabilities) + .config( + setupCompileProvider( + 'injectedMetadata' in newPlatform + ? newPlatform.injectedMetadata.getLegacyMetadata().devMode + : newPlatform.env.mode.dev + ) + ) .config(setupLocationProvider()) - .config($setupXsrfRequestInterceptor(newPlatform)) - .run(capture$httpLoadingCount(newPlatform)) - .run($setupBreadcrumbsAutoClear(newPlatform, isLocalAngular)) - .run($setupBadgeAutoClear(newPlatform, isLocalAngular)) - .run($setupHelpExtensionAutoClear(newPlatform, isLocalAngular)) - .run($setupUrlOverflowHandling(newPlatform, isLocalAngular)) - .run($setupUICapabilityRedirect(newPlatform)); + .config($setupXsrfRequestInterceptor(packageInfo.version)) + .run(capture$httpLoadingCount(core)) + .run($setupBreadcrumbsAutoClear(core, isLocalAngular)) + .run($setupBadgeAutoClear(core, isLocalAngular)) + .run($setupHelpExtensionAutoClear(core, isLocalAngular)) + .run($setupUrlOverflowHandling(core, isLocalAngular)) + .run($setupUICapabilityRedirect(core)); }; const getEsUrl = (newPlatform: CoreStart) => { @@ -122,10 +141,8 @@ const getEsUrl = (newPlatform: CoreStart) => { }; }; -const setupCompileProvider = (newPlatform: LegacyCoreStart) => ( - $compileProvider: ICompileProvider -) => { - if (!newPlatform.injectedMetadata.getLegacyMetadata().devMode) { +const setupCompileProvider = (devMode: boolean) => ($compileProvider: ICompileProvider) => { + if (!devMode) { $compileProvider.debugInfoEnabled(false); } }; @@ -140,9 +157,7 @@ const setupLocationProvider = () => ($locationProvider: ILocationProvider) => { $locationProvider.hashPrefix(''); }; -export const $setupXsrfRequestInterceptor = (newPlatform: LegacyCoreStart) => { - const version = newPlatform.injectedMetadata.getLegacyMetadata().version; - +export const $setupXsrfRequestInterceptor = (version: string) => { // Configure jQuery prefilter $.ajaxPrefilter(({ kbnXsrfToken = true }: any, originalOptions, jqXHR) => { if (kbnXsrfToken) { diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts index 19833d638fe4c4..18f01854de2599 100644 --- a/src/plugins/kibana_legacy/public/index.ts +++ b/src/plugins/kibana_legacy/public/index.ts @@ -24,6 +24,7 @@ export const plugin = (initializerContext: PluginInitializerContext) => new KibanaLegacyPlugin(initializerContext); export * from './plugin'; +export { kbnBaseUrl } from '../common/kbn_base_url'; export { initAngularBootstrap } from './angular_bootstrap'; export * from './angular'; diff --git a/src/plugins/kibana_legacy/public/mocks.ts b/src/plugins/kibana_legacy/public/mocks.ts index aab3ab315f0c63..8e9a05b186191f 100644 --- a/src/plugins/kibana_legacy/public/mocks.ts +++ b/src/plugins/kibana_legacy/public/mocks.ts @@ -17,6 +17,7 @@ * under the License. */ +import { EnvironmentMode, PackageInfo } from 'kibana/server'; import { KibanaLegacyPlugin } from './plugin'; export type Setup = jest.Mocked>; @@ -28,6 +29,10 @@ const createSetupContract = (): Setup => ({ config: { defaultAppId: 'home', }, + env: {} as { + mode: Readonly; + packageInfo: Readonly; + }, }); const createStartContract = (): Start => ({ diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index 86e56c44646c06..2ad620f355848a 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -107,7 +107,17 @@ export class KibanaLegacyPlugin { this.forwards.push({ legacyAppId, newAppId, ...options }); }, + /** + * @deprecated + * The `defaultAppId` config key is temporarily exposed to be used in the legacy platform. + * As this setting is going away, no new code should depend on it. + */ config: this.initializerContext.config.get(), + /** + * @deprecated + * Temporarily exposing the NP env to simulate initializer contexts in the LP. + */ + env: this.initializerContext.env, }; } diff --git a/src/plugins/kibana_legacy/server/index.ts b/src/plugins/kibana_legacy/server/index.ts index 4d0fe8364a66c5..98c754795e9472 100644 --- a/src/plugins/kibana_legacy/server/index.ts +++ b/src/plugins/kibana_legacy/server/index.ts @@ -32,6 +32,8 @@ export const config: PluginConfigDescriptor = { ], }; +export { kbnBaseUrl } from '../common/kbn_base_url'; + class Plugin { public setup(core: CoreSetup) {} diff --git a/test/functional/apps/dashboard/panel_controls.js b/test/functional/apps/dashboard/panel_controls.js index 5ec6cf3389c4ef..f30f58913bd970 100644 --- a/test/functional/apps/dashboard/panel_controls.js +++ b/test/functional/apps/dashboard/panel_controls.js @@ -54,8 +54,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.dashboard.gotoDashboardLandingPage(); }); - // unskip when issue is fixed https://github.com/elastic/kibana/issues/55992 - describe.skip('visualization object replace flyout', () => { + describe('visualization object replace flyout', () => { let intialDimensions; before(async () => { await PageObjects.dashboard.clickNewDashboard(); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx index 2f7df3c5a4acd7..3be096d9db2bcd 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -22,6 +22,7 @@ import { ServiceNodeMetrics } from '../../ServiceNodeMetrics'; import { resolveUrlParams } from '../../../../context/UrlParamsContext/resolveUrlParams'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n'; import { TraceLink } from '../../TraceLink'; +import { CustomizeUI } from '../../Settings/CustomizeUI'; const metricsBreadcrumb = i18n.translate('xpack.apm.breadcrumb.metricsTitle', { defaultMessage: 'Metrics' @@ -212,5 +213,18 @@ export const routes: BreadcrumbRoute[] = [ defaultMessage: 'Service Map' }), name: RouteName.SINGLE_SERVICE_MAP + }, + { + exact: true, + path: '/settings/customize-ui', + component: () => ( + + + + ), + breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.customizeUI', { + defaultMessage: 'Customize UI' + }), + name: RouteName.CUSTOMIZE_UI } ]; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx index 0ae7a948be4e18..db57e8356f39b2 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx @@ -22,5 +22,6 @@ export enum RouteName { AGENT_CONFIGURATION = 'agent_configuration', INDICES = 'indices', SERVICE_NODES = 'nodes', - LINK_TO_TRACE = 'link_to_trace' + LINK_TO_TRACE = 'link_to_trace', + CUSTOMIZE_UI = 'customize_ui' } diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx index e1cb07be3d3786..7243a86404f045 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx @@ -26,7 +26,7 @@ import { useCallApmApi } from '../../../../../hooks/useCallApmApi'; import { transactionSampleRateRt } from '../../../../../../common/runtime_types/transaction_sample_rate_rt'; import { Config } from '../index'; import { SettingsSection } from './SettingsSection'; -import { ServiceSection } from './ServiceSection'; +import { ServiceForm } from '../../../../shared/ServiceForm'; import { DeleteButton } from './DeleteButton'; import { transactionMaxSpansRt } from '../../../../../../common/runtime_types/transaction_max_spans_rt'; import { useFetcher } from '../../../../../hooks/useFetcher'; @@ -176,16 +176,16 @@ export function AddEditFlyout({ } }} > - diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/SettingsSection.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/SettingsSection.tsx new file mode 100644 index 00000000000000..8cb604d3675497 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/SettingsSection.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiFieldText, EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +interface Props { + label: string; + onLabelChange: (label: string) => void; + url: string; + onURLChange: (url: string) => void; +} + +export const SettingsSection = ({ + label, + onLabelChange, + url, + onURLChange +}: Props) => { + return ( + <> + +

+ {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.flyout.settingsSection.title', + { defaultMessage: 'Action' } + )} +

+
+ + + { + onLabelChange(e.target.value); + }} + /> + + + { + onURLChange(e.target.value); + }} + /> + + + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/index.tsx new file mode 100644 index 00000000000000..d04cdd62c303ba --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/index.tsx @@ -0,0 +1,109 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiPortal, + EuiSpacer, + EuiText, + EuiTitle +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import { SettingsSection } from './SettingsSection'; +import { ServiceForm } from '../../../../../shared/ServiceForm'; + +interface Props { + onClose: () => void; +} + +export const CustomActionsFlyout = ({ onClose }: Props) => { + const [serviceName, setServiceName] = useState(''); + const [environment, setEnvironment] = useState(''); + const [label, setLabel] = useState(''); + const [url, setURL] = useState(''); + return ( + + + + +

+ {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.flyout.title', + { + defaultMessage: 'Create custom action' + } + )} +

+
+
+ + +

+ {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.flyout.label', + { + defaultMessage: + "This action will be shown in the 'Actions' context menu for the trace and error detail components. You can specify any number of links, but only the first three will be shown, in alphabetical order." + } + )} +

+
+ + + + + + +
+ + + + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.flyout.close', + { + defaultMessage: 'Close' + } + )} + + + + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.flyout.save', + { + defaultMessage: 'Save' + } + )} + + + + +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/EmptyPrompt.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/EmptyPrompt.tsx new file mode 100644 index 00000000000000..f39e4b307b24cc --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/EmptyPrompt.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export const EmptyPrompt = ({ + onCreateCustomActionClick +}: { + onCreateCustomActionClick: () => void; +}) => { + return ( + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.emptyPromptTitle', + { + defaultMessage: 'No actions found.' + } + )} + + } + body={ + <> +

+ {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.emptyPromptText', + { + defaultMessage: + "Let's change that! You can add custom actions to the Actions context menu by the trace and error details for each service. This could be linking to a Kibana dashboard or going to your organization's support portal" + } + )} +

+ + } + actions={ + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.createCustomAction', + { defaultMessage: 'Create custom action' } + )} + + } + /> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/Title.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/Title.tsx new file mode 100644 index 00000000000000..d7f90e0919733f --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/Title.tsx @@ -0,0 +1,40 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export const Title = () => ( + + + + + +

+ {i18n.translate('xpack.apm.settings.customizeUI.customActions', { + defaultMessage: 'Custom actions' + })} +

+
+ + + + +
+
+
+
+); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/__test__/CustomActions.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/__test__/CustomActions.test.tsx new file mode 100644 index 00000000000000..970de66c64a9aa --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/__test__/CustomActions.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { fireEvent, render } from '@testing-library/react'; +import { CustomActionsOverview } from '../'; +import { expectTextsInDocument } from '../../../../../../utils/testHelpers'; +import * as hooks from '../../../../../../hooks/useFetcher'; + +describe('CustomActions', () => { + afterEach(() => jest.restoreAllMocks()); + + describe('empty prompt', () => { + it('shows when any actions are available', () => { + // TODO: mock return items + const component = render(); + expectTextsInDocument(component, ['No actions found.']); + }); + it('opens flyout when click to create new action', () => { + spyOn(hooks, 'useFetcher').and.returnValue({ + data: [], + status: 'success' + }); + const { queryByText, getByText } = render(); + expect(queryByText('Service')).not.toBeInTheDocument(); + fireEvent.click(getByText('Create custom action')); + expect(queryByText('Service')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/index.tsx new file mode 100644 index 00000000000000..ae2972f251fc28 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/index.tsx @@ -0,0 +1,75 @@ +/* + * 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 { EuiPanel, EuiSpacer } from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import React, { useState } from 'react'; +import { ManagedTable } from '../../../../shared/ManagedTable'; +import { Title } from './Title'; +import { EmptyPrompt } from './EmptyPrompt'; +import { CustomActionsFlyout } from './CustomActionsFlyout'; + +export const CustomActionsOverview = () => { + const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + + // TODO: change it to correct fields fetched from ES + const columns = [ + { + field: 'actionName', + name: 'Action Name', + truncateText: true + }, + { + field: 'serviceName', + name: 'Service Name' + }, + { + field: 'environment', + name: 'Environment' + }, + { + field: 'lastUpdate', + name: 'Last update' + }, + { + field: 'actions', + name: 'Actions' + } + ]; + + // TODO: change to items fetched from ES. + const items: object[] = []; + + const onCloseFlyout = () => { + setIsFlyoutOpen(false); + }; + + const onCreateCustomActionClick = () => { + setIsFlyoutOpen(true); + }; + + return ( + <> + + + <EuiSpacer size="m" /> + {isFlyoutOpen && <CustomActionsFlyout onClose={onCloseFlyout} />} + {isEmpty(items) ? ( + <EmptyPrompt onCreateCustomActionClick={onCreateCustomActionClick} /> + ) : ( + <ManagedTable + items={items} + columns={columns} + initialPageSize={25} + initialSortField="occurrenceCount" + initialSortDirection="desc" + sortItems={false} + /> + )} + </EuiPanel> + </> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx new file mode 100644 index 00000000000000..17a4b2f8476795 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx @@ -0,0 +1,26 @@ +/* + * 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 { EuiTitle, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { CustomActionsOverview } from './CustomActionsOverview'; + +export const CustomizeUI = () => { + return ( + <> + <EuiTitle size="l"> + <h1> + {i18n.translate('xpack.apm.settings.customizeUI', { + defaultMessage: 'Customize UI' + })} + </h1> + </EuiTitle> + <EuiSpacer size="l" /> + <CustomActionsOverview /> + </> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx index f3be5abe4d48ba..eef386731c5c36 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx @@ -39,20 +39,34 @@ export const Settings: React.FC = props => { id: 0, items: [ { - name: 'Agent Configuration', + name: i18n.translate( + 'xpack.apm.settings.agentConfiguration', + { + defaultMessage: 'Agent Configuration' + } + ), id: '1', // @ts-ignore href: getAPMHref('/settings/agent-configuration', search), - // @ts-ignore isSelected: pathname === '/settings/agent-configuration' }, { - name: 'Indices', + name: i18n.translate('xpack.apm.settings.indices', { + defaultMessage: 'Indices' + }), id: '2', // @ts-ignore href: getAPMHref('/settings/apm-indices', search), - // @ts-ignore isSelected: pathname === '/settings/apm-indices' + }, + { + name: i18n.translate('xpack.apm.settings.customizeUI', { + defaultMessage: 'Customize UI' + }), + id: '3', + // @ts-ignore + href: getAPMHref('/settings/customize-ui', search), + isSelected: pathname === '/settings/customize-ui' } ] } diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx b/x-pack/legacy/plugins/apm/public/components/shared/ServiceForm/index.tsx similarity index 76% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx rename to x-pack/legacy/plugins/apm/public/components/shared/ServiceForm/index.tsx index 513dfceaa3ae2c..58a203bded7156 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/ServiceForm/index.tsx @@ -7,32 +7,32 @@ import { EuiTitle, EuiSpacer, EuiFormRow, EuiText } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import { SelectWithPlaceholder } from '../../../../shared/SelectWithPlaceholder'; -import { useFetcher } from '../../../../../hooks/useFetcher'; import { - getOptionLabel, - omitAllOption -} from '../../../../../../common/agent_configuration_constants'; + omitAllOption, + getOptionLabel +} from '../../../../common/agent_configuration_constants'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { SelectWithPlaceholder } from '../SelectWithPlaceholder'; const SELECT_PLACEHOLDER_LABEL = `- ${i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSection.selectPlaceholder', + 'xpack.apm.settings.agentConf.flyOut.serviceForm.selectPlaceholder', { defaultMessage: 'Select' } )} -`; interface Props { isReadOnly: boolean; serviceName: string; - setServiceName: (env: string) => void; + onServiceNameChange: (env: string) => void; environment: string; - setEnvironment: (env: string) => void; + onEnvironmentChange: (env: string) => void; } -export function ServiceSection({ +export function ServiceForm({ isReadOnly, serviceName, - setServiceName, + onServiceNameChange, environment, - setEnvironment + onEnvironmentChange }: Props) { const { data: serviceNames = [], status: serviceNamesStatus } = useFetcher( callApmApi => { @@ -60,7 +60,7 @@ export function ServiceSection({ ); const ALREADY_CONFIGURED_TRANSLATED = i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSection.alreadyConfiguredOption', + 'xpack.apm.settings.agentConf.flyOut.serviceForm.alreadyConfiguredOption', { defaultMessage: 'already configured' } ); @@ -83,7 +83,7 @@ export function ServiceSection({ <EuiTitle size="xs"> <h3> {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSection.title', + 'xpack.apm.settings.agentConf.flyOut.serviceForm.title', { defaultMessage: 'Service' } )} </h3> @@ -93,13 +93,13 @@ export function ServiceSection({ <EuiFormRow label={i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSection.serviceNameSelectLabel', + 'xpack.apm.settings.agentConf.flyOut.serviceForm.serviceNameSelectLabel', { defaultMessage: 'Name' } )} helpText={ !isReadOnly && i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSection.serviceNameSelectHelpText', + 'xpack.apm.settings.agentConf.flyOut.serviceForm.serviceNameSelectHelpText', { defaultMessage: 'Choose the service you want to configure.' } ) } @@ -115,8 +115,8 @@ export function ServiceSection({ disabled={serviceNamesStatus === 'loading'} onChange={e => { e.preventDefault(); - setServiceName(e.target.value); - setEnvironment(''); + onServiceNameChange(e.target.value); + onEnvironmentChange(''); }} /> )} @@ -124,13 +124,13 @@ export function ServiceSection({ <EuiFormRow label={i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSection.serviceEnvironmentSelectLabel', + 'xpack.apm.settings.agentConf.flyOut.serviceForm.serviceEnvironmentSelectLabel', { defaultMessage: 'Environment' } )} helpText={ !isReadOnly && i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSection.serviceEnvironmentSelectHelpText', + 'xpack.apm.settings.agentConf.flyOut.serviceForm.serviceEnvironmentSelectHelpText', { defaultMessage: 'Only a single environment per configuration is supported.' @@ -149,7 +149,7 @@ export function ServiceSection({ disabled={!serviceName || environmentStatus === 'loading'} onChange={e => { e.preventDefault(); - setEnvironment(e.target.value); + onEnvironmentChange(e.target.value); }} /> )} diff --git a/x-pack/legacy/plugins/canvas/public/legacy.ts b/x-pack/legacy/plugins/canvas/public/legacy.ts index ea873e6f2296d2..0d2e77637f19d5 100644 --- a/x-pack/legacy/plugins/canvas/public/legacy.ts +++ b/x-pack/legacy/plugins/canvas/public/legacy.ts @@ -10,7 +10,6 @@ import { CanvasStartDeps } from './plugin'; // eslint-disable-line import/order // @ts-ignore Untyped Kibana Lib import chrome, { loadingCount } from 'ui/chrome'; // eslint-disable-line import/order import { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; // eslint-disable-line import/order -import { Storage } from '../../../../../src/plugins/kibana_utils/public'; // eslint-disable-line import/order // @ts-ignore Untyped Kibana Lib import { formatMsg } from '../../../../../src/plugins/kibana_legacy/public'; // eslint-disable-line import/order @@ -31,7 +30,6 @@ const shimStartPlugins: CanvasStartDeps = { absoluteToParsedUrl, // ToDo: Copy directly into canvas formatMsg, - storage: Storage, // ToDo: Won't be a part of New Platform. Will need to handle internally trackSubUrlForApp: chrome.trackSubUrlForApp, }, diff --git a/x-pack/legacy/plugins/canvas/public/lib/__tests__/clipboard.js b/x-pack/legacy/plugins/canvas/public/lib/__tests__/clipboard.js deleted file mode 100644 index c616a76d0dcc32..00000000000000 --- a/x-pack/legacy/plugins/canvas/public/lib/__tests__/clipboard.js +++ /dev/null @@ -1,16 +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 expect from '@kbn/expect'; -import { setClipboardData, getClipboardData } from '../clipboard'; -import { elements } from '../../../__tests__/fixtures/workpads'; - -describe('clipboard', () => { - it('stores and retrieves clipboard data', () => { - setClipboardData(elements); - expect(getClipboardData()).to.eql(JSON.stringify(elements)); - }); -}); diff --git a/x-pack/legacy/plugins/canvas/public/lib/clipboard.js b/x-pack/legacy/plugins/canvas/public/lib/clipboard.js deleted file mode 100644 index 1fd14f086c9493..00000000000000 --- a/x-pack/legacy/plugins/canvas/public/lib/clipboard.js +++ /dev/null @@ -1,17 +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 { LOCALSTORAGE_CLIPBOARD } from '../../common/lib/constants'; -import { getWindow } from './get_window'; - -let storage = null; - -export const initClipboard = function(Storage) { - storage = new Storage(getWindow().localStorage); -}; - -export const setClipboardData = data => storage.set(LOCALSTORAGE_CLIPBOARD, JSON.stringify(data)); -export const getClipboardData = () => storage.get(LOCALSTORAGE_CLIPBOARD); diff --git a/x-pack/legacy/plugins/canvas/public/lib/clipboard.test.ts b/x-pack/legacy/plugins/canvas/public/lib/clipboard.test.ts new file mode 100644 index 00000000000000..54c3000dae36ca --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/lib/clipboard.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +jest.mock('../../../../../../src/plugins/kibana_utils/public'); + +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import { setClipboardData, getClipboardData } from './clipboard'; +import { LOCALSTORAGE_CLIPBOARD } from '../../common/lib/constants'; +import { elements } from '../../__tests__/fixtures/workpads'; + +const set = jest.fn(); +const get = jest.fn(); + +describe('clipboard', () => { + beforeAll(() => { + // @ts-ignore + Storage.mockImplementation(() => ({ + set, + get, + })); + }); + + test('stores data to local storage', () => { + setClipboardData(elements); + + expect(set).toBeCalledWith(LOCALSTORAGE_CLIPBOARD, JSON.stringify(elements)); + }); + + test('gets data from local storage', () => { + getClipboardData(); + + expect(get).toBeCalledWith(LOCALSTORAGE_CLIPBOARD); + }); +}); diff --git a/x-pack/legacy/plugins/canvas/public/lib/clipboard.ts b/x-pack/legacy/plugins/canvas/public/lib/clipboard.ts new file mode 100644 index 00000000000000..50c5cdd0042fd4 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/lib/clipboard.ts @@ -0,0 +1,25 @@ +/* + * 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 { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import { LOCALSTORAGE_CLIPBOARD } from '../../common/lib/constants'; +import { getWindow } from './get_window'; + +let storage: Storage; + +const getStorage = (): Storage => { + if (!storage) { + storage = new Storage(getWindow().localStorage); + } + + return storage; +}; + +export const setClipboardData = (data: any) => { + getStorage().set(LOCALSTORAGE_CLIPBOARD, JSON.stringify(data)); +}; + +export const getClipboardData = () => getStorage().get(LOCALSTORAGE_CLIPBOARD); diff --git a/x-pack/legacy/plugins/canvas/public/lib/get_window.ts b/x-pack/legacy/plugins/canvas/public/lib/get_window.ts index 1ee7e68be84853..42c632f4a514f5 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/get_window.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/get_window.ts @@ -5,8 +5,10 @@ */ // return window if it exists, otherwise just return an object literal -const windowObj = { location: null }; +const windowObj = { location: null, localStorage: {} as Window['localStorage'] }; -export const getWindow = (): Window | { location: Location | null } => { +export const getWindow = (): + | Window + | { location: Location | null; localStorage: Window['localStorage'] } => { return typeof window === 'undefined' ? windowObj : window; }; diff --git a/x-pack/legacy/plugins/canvas/public/plugin.tsx b/x-pack/legacy/plugins/canvas/public/plugin.tsx index 44731628cf6535..a5fbbccb4299fe 100644 --- a/x-pack/legacy/plugins/canvas/public/plugin.tsx +++ b/x-pack/legacy/plugins/canvas/public/plugin.tsx @@ -8,7 +8,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Chrome } from 'ui/chrome'; import { i18n } from '@kbn/i18n'; -import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { CoreSetup, CoreStart, Plugin } from '../../../../../src/core/public'; import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; // @ts-ignore: Untyped Local @@ -43,7 +42,6 @@ export interface CanvasStartDeps { __LEGACY: { absoluteToParsedUrl: (url: string, basePath: string) => any; formatMsg: any; - storage: typeof Storage; trackSubUrlForApp: Chrome['trackSubUrlForApp']; }; } @@ -92,7 +90,6 @@ export class CanvasPlugin loadExpressionTypes(); loadTransitions(); - initClipboard(plugins.__LEGACY.storage); initLoadingIndicator(core.http.addLoadingCountSource); core.chrome.setBadge( diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts index d0797e716d84e4..4ccaf6b5dfa277 100644 --- a/x-pack/legacy/plugins/graph/public/plugin.ts +++ b/x-pack/legacy/plugins/graph/public/plugin.ts @@ -5,7 +5,13 @@ */ // NP type imports -import { CoreSetup, CoreStart, Plugin, SavedObjectsClientContract } from 'src/core/public'; +import { + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, + SavedObjectsClientContract, +} from 'src/core/public'; import { Plugin as DataPlugin } from 'src/plugins/data/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; @@ -33,7 +39,8 @@ export class GraphPlugin implements Plugin { core.application.register({ id: 'graph', title: 'Graph', - mount: async ({ core: contextCore }, params) => { + mount: async (params: AppMountParameters) => { + const [coreStart] = await core.getStartServices(); const { renderApp } = await import('./application'); return renderApp({ ...params, @@ -46,13 +53,13 @@ export class GraphPlugin implements Plugin { canEditDrillDownUrls: graph.config.canEditDrillDownUrls, graphSavePolicy: graph.config.savePolicy, storage: new Storage(window.localStorage), - capabilities: contextCore.application.capabilities.graph, - coreStart: contextCore, - chrome: contextCore.chrome, - config: contextCore.uiSettings, - toastNotifications: contextCore.notifications.toasts, + capabilities: coreStart.application.capabilities.graph, + coreStart, + chrome: coreStart.chrome, + config: coreStart.uiSettings, + toastNotifications: coreStart.notifications.toasts, indexPatterns: this.npDataStart!.indexPatterns, - overlays: contextCore.overlays, + overlays: coreStart.overlays, }); }, }); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap index 101716d297b81e..7997cde97d89ca 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap @@ -101,7 +101,10 @@ exports[`LayerPanel is rendered 1`] = ` mockSourceSettings </div> <EuiPanel> - <JoinEditor /> + <JoinEditor + layerDisplayName="layer 1" + leftJoinFields={Array []} + /> </EuiPanel> <EuiSpacer size="s" diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/index.js index 89ab7cf927d5b1..1340d4fd4f2199 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/index.js @@ -10,8 +10,10 @@ import { getSelectedLayer } from '../../selectors/map_selectors'; import { fitToLayerExtent, updateSourceProp } from '../../actions/map_actions'; function mapStateToProps(state = {}) { + const selectedLayer = getSelectedLayer(state); return { - selectedLayer: getSelectedLayer(state), + key: selectedLayer ? selectedLayer.getId() : '', + selectedLayer, }; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js index 8660fa6010f8a6..c2c9f333a675cc 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js @@ -16,49 +16,22 @@ import { GlobalFilterCheckbox } from '../../../../components/global_filter_check import { indexPatterns } from '../../../../../../../../../src/plugins/data/public'; import { indexPatternService } from '../../../../kibana_services'; -const getIndexPatternId = props => { - return _.get(props, 'join.right.indexPatternId'); -}; - export class Join extends Component { state = { - leftFields: null, - leftSourceName: '', rightFields: undefined, indexPattern: undefined, loadError: undefined, - prevIndexPatternId: getIndexPatternId(this.props), }; componentDidMount() { this._isMounted = true; - this._loadLeftFields(); - this._loadLeftSourceName(); + this._loadRightFields(_.get(this.props.join, 'right.indexPatternId')); } componentWillUnmount() { this._isMounted = false; } - componentDidUpdate() { - if (!this.state.rightFields && getIndexPatternId(this.props) && !this.state.loadError) { - this._loadRightFields(getIndexPatternId(this.props)); - } - } - - static getDerivedStateFromProps(nextProps, prevState) { - const nextIndexPatternId = getIndexPatternId(nextProps); - if (nextIndexPatternId !== prevState.prevIndexPatternId) { - return { - rightFields: undefined, - loadError: undefined, - prevIndexPatternId: nextIndexPatternId, - }; - } - - return null; - } - async _loadRightFields(indexPatternId) { if (!indexPatternId) { return; @@ -79,11 +52,6 @@ export class Join extends Component { return; } - if (indexPatternId !== this.state.prevIndexPatternId) { - // ignore out of order responses - return; - } - if (!this._isMounted) { return; } @@ -94,34 +62,6 @@ export class Join extends Component { }); } - async _loadLeftSourceName() { - const leftSourceName = await this.props.layer.getSourceName(); - if (!this._isMounted) { - return; - } - this.setState({ leftSourceName }); - } - - async _loadLeftFields() { - let leftFields; - try { - const leftFieldsInstances = await this.props.layer.getLeftJoinFields(); - const leftFieldPromises = leftFieldsInstances.map(async field => { - return { - name: field.getName(), - label: await field.getLabel(), - }; - }); - leftFields = await Promise.all(leftFieldPromises); - } catch (error) { - leftFields = []; - } - if (!this._isMounted) { - return; - } - this.setState({ leftFields }); - } - _onLeftFieldChange = leftField => { this.props.onChange({ leftField: leftField, @@ -130,6 +70,11 @@ export class Join extends Component { }; _onRightSourceChange = ({ indexPatternId, indexPatternTitle }) => { + this.setState({ + rightFields: undefined, + loadError: undefined, + }); + this._loadRightFields(indexPatternId); this.props.onChange({ leftField: this.props.join.leftField, right: { @@ -181,8 +126,8 @@ export class Join extends Component { }; render() { - const { join, onRemove } = this.props; - const { leftSourceName, leftFields, rightFields, indexPattern } = this.state; + const { join, onRemove, leftFields, leftSourceName } = this.props; + const { rightFields, indexPattern } = this.state; const right = _.get(join, 'right', {}); const rightSourceName = right.indexPatternTitle ? right.indexPatternTitle diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/view.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/view.js index 9f3461e45dfd4d..92e32885d43a8f 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/view.js @@ -20,7 +20,7 @@ import { Join } from './resources/join'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -export function JoinEditor({ joins, layer, onChange }) { +export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDisplayName }) { const renderJoins = () => { return joins.map((joinDescriptor, index) => { const handleOnChange = updatedDescriptor => { @@ -39,6 +39,8 @@ export function JoinEditor({ joins, layer, onChange }) { layer={layer} onChange={handleOnChange} onRemove={handleOnRemove} + leftFields={leftJoinFields} + leftSourceName={layerDisplayName} /> </Fragment> ); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js index 50c0949cf91ae6..755d4bb6b323ac 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js @@ -37,30 +37,17 @@ const localStorage = new Storage(window.localStorage); import { npStart } from 'ui/new_platform'; export class LayerPanel extends React.Component { - static getDerivedStateFromProps(nextProps, prevState) { - const nextId = nextProps.selectedLayer ? nextProps.selectedLayer.getId() : null; - if (nextId !== prevState.prevId) { - return { - displayName: '', - immutableSourceProps: [], - hasLoadedSourcePropsForLayer: false, - prevId: nextId, - }; - } - return null; - } - - state = {}; + state = { + displayName: '', + immutableSourceProps: [], + leftJoinFields: null, + }; componentDidMount() { this._isMounted = true; this.loadDisplayName(); this.loadImmutableSourceProperties(); - } - - componentDidUpdate() { - this.loadDisplayName(); - this.loadImmutableSourceProperties(); + this.loadLeftJoinFields(); } componentWillUnmount() { @@ -73,27 +60,45 @@ export class LayerPanel extends React.Component { } const displayName = await this.props.selectedLayer.getDisplayName(); - if (!this._isMounted || displayName === this.state.displayName) { - return; + if (this._isMounted) { + this.setState({ displayName }); } - - this.setState({ displayName }); }; loadImmutableSourceProperties = async () => { - if (this.state.hasLoadedSourcePropsForLayer || !this.props.selectedLayer) { + if (!this.props.selectedLayer) { return; } const immutableSourceProps = await this.props.selectedLayer.getImmutableSourceProperties(); if (this._isMounted) { - this.setState({ - immutableSourceProps, - hasLoadedSourcePropsForLayer: true, - }); + this.setState({ immutableSourceProps }); } }; + async loadLeftJoinFields() { + if (!this.props.selectedLayer || !this.props.selectedLayer.isJoinable()) { + return; + } + + let leftJoinFields; + try { + const leftFieldsInstances = await this.props.selectedLayer.getLeftJoinFields(); + const leftFieldPromises = leftFieldsInstances.map(async field => { + return { + name: field.getName(), + label: await field.getLabel(), + }; + }); + leftJoinFields = await Promise.all(leftFieldPromises); + } catch (error) { + leftJoinFields = []; + } + if (this._isMounted) { + this.setState({ leftJoinFields }); + } + } + _onSourceChange = ({ propName, value }) => { this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value); }; @@ -121,7 +126,10 @@ export class LayerPanel extends React.Component { return ( <Fragment> <EuiPanel> - <JoinEditor /> + <JoinEditor + leftJoinFields={this.state.leftJoinFields} + layerDisplayName={this.state.displayName} + /> </EuiPanel> <EuiSpacer size="s" /> </Fragment> diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 31c3831fb612a6..1698d52ea4406d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -187,10 +187,6 @@ export class VectorLayer extends AbstractLayer { return await this._source.getLeftJoinFields(); } - async getSourceName() { - return this._source.getDisplayName(); - } - _getJoinFields() { const joinFields = []; this.getValidJoins().forEach(join => { @@ -272,7 +268,7 @@ export class VectorLayer extends AbstractLayer { try { startLoading(sourceDataId, requestToken, searchFilters); - const leftSourceName = await this.getSourceName(); + const leftSourceName = await this._source.getDisplayName(); const { propertiesMap } = await joinSource.getPropertiesMap( searchFilters, leftSourceName, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_detector/detector_cards.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_detector/detector_cards.tsx index 68d5fc24a96e3c..991e1d5c49b7a6 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_detector/detector_cards.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_detector/detector_cards.tsx @@ -17,6 +17,7 @@ interface CardProps { export const CountCard: FC<CardProps> = ({ onClick, isSelected }) => ( <EuiFlexItem> <EuiCard + data-test-subj={`mlJobWizardCategorizationDetectorCountCard${isSelected ? ' selected' : ''}`} title={i18n.translate( 'xpack.ml.newJob.wizard.pickFieldsStep.categorizationDetectorSelect.countCard.title', { @@ -39,6 +40,7 @@ export const CountCard: FC<CardProps> = ({ onClick, isSelected }) => ( export const RareCard: FC<CardProps> = ({ onClick, isSelected }) => ( <EuiFlexItem> <EuiCard + data-test-subj={`mlJobWizardCategorizationDetectorRareCard${isSelected ? ' selected' : ''}`} title={i18n.translate( 'xpack.ml.newJob.wizard.pickFieldsStep.categorizationDetectorSelect.rareCard.title', { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/examples_valid_callout.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/examples_valid_callout.tsx index ac886a3aea61a7..1265063b8aa813 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/examples_valid_callout.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/examples_valid_callout.tsx @@ -56,7 +56,11 @@ export const ExamplesValidCallout: FC<Props> = ({ } return ( - <EuiCallOut color={color} title={title}> + <EuiCallOut + color={color} + title={title} + data-test-subj={`mlJobWizardCategorizationExamplesCallout ${overallValidStatus}`} + > {validationChecks.map((v, i) => ( <div key={i}>{v.message}</div> ))} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx index 51cea179a6c0d9..d3f1f0e58698b5 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx @@ -57,7 +57,13 @@ export const FieldExamples: FC<Props> = ({ fieldExamples }) => { txt.push(buffer); return { example: txt }; }); - return <EuiBasicTable columns={columns} items={items} />; + return ( + <EuiBasicTable + columns={columns} + items={items} + data-test-subj="mlJobWizardCategorizationExamplesTable" + /> + ); }; const Token: FC = ({ children }) => ( diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 3c639239757dba..bafb12de068bbb 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -225,8 +225,8 @@ class TimeseriesChartIntl extends Component { this.renderFocusChart(); } - componentDidUpdate() { - if (this.props.renderFocusChartOnly === false) { + componentDidUpdate(prevProps) { + if (this.props.renderFocusChartOnly === false || prevProps.svgWidth !== this.props.svgWidth) { this.renderChart(); this.drawContextChartSelection(); } @@ -424,11 +424,8 @@ class TimeseriesChartIntl extends Component { } focusLoadTo = Math.min(focusLoadTo, contextXMax); - const brushVisibility = focusLoadFrom !== contextXMin || focusLoadTo !== contextXMax; - this.setBrushVisibility(brushVisibility); - if (focusLoadFrom !== contextXMin || focusLoadTo !== contextXMax) { - this.setContextBrushExtent(new Date(focusLoadFrom), new Date(focusLoadTo), true); + this.setContextBrushExtent(new Date(focusLoadFrom), new Date(focusLoadTo)); const newSelectedBounds = { min: moment(new Date(focusLoadFrom)), max: moment(focusLoadFrom), @@ -442,6 +439,10 @@ class TimeseriesChartIntl extends Component { }; if (!_.isEqual(newSelectedBounds, this.selectedBounds)) { this.selectedBounds = newSelectedBounds; + this.setContextBrushExtent( + new Date(contextXScaleDomain[0]), + new Date(contextXScaleDomain[1]) + ); if (this.contextChartInitialized === false) { this.contextChartInitialized = true; contextChartSelected({ from: contextXScaleDomain[0], to: contextXScaleDomain[1] }); @@ -1178,36 +1179,29 @@ class TimeseriesChartIntl extends Component { '<div class="brush-handle-inner brush-handle-inner-right"><i class="fa fa-caret-right"></i></div>' ); - const showBrush = show => { - if (show === true) { - const brushExtent = brush.extent(); - mask.reveal(brushExtent); - leftHandle.attr('x', contextXScale(brushExtent[0]) - 10); - rightHandle.attr('x', contextXScale(brushExtent[1]) + 0); - - topBorder.attr('x', contextXScale(brushExtent[0]) + 1); - // Use Math.max(0, ...) to make sure we don't end up - // with a negative width which would cause an SVG error. - topBorder.attr( - 'width', - Math.max(0, contextXScale(brushExtent[1]) - contextXScale(brushExtent[0]) - 2) - ); - } - - this.setBrushVisibility(show); - }; - - showBrush(!brush.empty()); - function brushing() { + const brushExtent = brush.extent(); + mask.reveal(brushExtent); + leftHandle.attr('x', contextXScale(brushExtent[0]) - 10); + rightHandle.attr('x', contextXScale(brushExtent[1]) + 0); + + topBorder.attr('x', contextXScale(brushExtent[0]) + 1); + // Use Math.max(0, ...) to make sure we don't end up + // with a negative width which would cause an SVG error. + const topBorderWidth = Math.max( + 0, + contextXScale(brushExtent[1]) - contextXScale(brushExtent[0]) - 2 + ); + topBorder.attr('width', topBorderWidth); + const isEmpty = brush.empty(); - showBrush(!isEmpty); + d3.selectAll('.brush-handle').style('visibility', isEmpty ? 'hidden' : 'visible'); } + brushing(); const that = this; function brushed() { const isEmpty = brush.empty(); - const selectedBounds = isEmpty ? contextXScale.domain() : brush.extent(); const selectionMin = selectedBounds[0].getTime(); const selectionMax = selectedBounds[1].getTime(); @@ -1221,8 +1215,6 @@ class TimeseriesChartIntl extends Component { return; } - showBrush(!isEmpty); - // Set the color of the swimlane cells according to whether they are inside the selection. contextGroup.selectAll('.swimlane-cell').style('fill', d => { const cellMs = d.date.getTime(); @@ -1238,26 +1230,6 @@ class TimeseriesChartIntl extends Component { } }; - setBrushVisibility = show => { - const mask = this.mask; - - if (mask !== undefined) { - const visibility = show ? 'visible' : 'hidden'; - mask.style('visibility', visibility); - - d3.selectAll('.brush').style('visibility', visibility); - - const brushHandles = d3.selectAll('.brush-handle-inner'); - brushHandles.style('visibility', visibility); - - const topBorder = d3.selectAll('.top-border'); - topBorder.style('visibility', visibility); - - const border = d3.selectAll('.chart-border-highlight'); - border.style('visibility', visibility); - } - }; - drawSwimlane = (swlGroup, swlWidth, swlHeight) => { const { contextAggregationInterval, swimlaneData } = this.props; @@ -1368,21 +1340,18 @@ class TimeseriesChartIntl extends Component { // Sets the extent of the brush on the context chart to the // supplied from and to Date objects. - setContextBrushExtent = (from, to, fireEvent) => { + setContextBrushExtent = (from, to) => { const brush = this.brush; const brushExtent = brush.extent(); const newExtent = [from, to]; - if ( - newExtent[0].getTime() === brushExtent[0].getTime() && - newExtent[1].getTime() === brushExtent[1].getTime() - ) { - fireEvent = false; - } - brush.extent(newExtent); brush(d3.select('.brush')); - if (fireEvent) { + + if ( + newExtent[0].getTime() !== brushExtent[0].getTime() || + newExtent[1].getTime() !== brushExtent[1].getTime() + ) { brush.event(d3.select('.brush')); } }; @@ -1403,7 +1372,7 @@ class TimeseriesChartIntl extends Component { to = Math.min(minBoundsMs + millis, maxBoundsMs); } - this.setContextBrushExtent(new Date(from), new Date(to), true); + this.setContextBrushExtent(new Date(from), new Date(to)); } showFocusChartTooltip(marker, circle) { diff --git a/x-pack/legacy/plugins/monitoring/common/formatting.js b/x-pack/legacy/plugins/monitoring/common/formatting.js index a3b3ce07c8c760..ed5d68f942dfd1 100644 --- a/x-pack/legacy/plugins/monitoring/common/formatting.js +++ b/x-pack/legacy/plugins/monitoring/common/formatting.js @@ -13,14 +13,16 @@ export const SMALL_BYTES = '0.0 b'; export const LARGE_ABBREVIATED = '0,0.[0]a'; /** - * Format the {@code date} in the user's expected date/time format using their <em>guessed</em> local time zone. + * Format the {@code date} in the user's expected date/time format using their <em>dateFormat:tz</em> defined time zone. * @param date Either a numeric Unix timestamp or a {@code Date} object * @returns The date formatted using 'LL LTS' */ -export function formatDateTimeLocal(date, useUTC = false) { - return useUTC - ? moment.utc(date).format('LL LTS') - : moment.tz(date, moment.tz.guess()).format('LL LTS'); +export function formatDateTimeLocal(date, timezone) { + if (timezone === 'Browser') { + timezone = moment.tz.guess() || 'utc'; + } + + return moment.tz(date, timezone).format('LL LTS'); } /** diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js b/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js index 4c2f3b027bc8ae..11fcef73a4b976 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js @@ -5,6 +5,7 @@ */ import React from 'react'; +import chrome from '../../np_imports/ui/chrome'; import { capitalize } from 'lodash'; import { formatDateTimeLocal } from '../../../common/formatting'; import { formatTimestampToDuration } from '../../../common'; @@ -21,7 +22,7 @@ const linkToCategories = { 'kibana/instances': 'Kibana Instances', 'logstash/instances': 'Logstash Nodes', }; -const getColumns = (kbnUrl, scope) => [ +const getColumns = (kbnUrl, scope, timezone) => [ { name: i18n.translate('xpack.monitoring.alerts.statusColumnTitle', { defaultMessage: 'Status', @@ -126,7 +127,7 @@ const getColumns = (kbnUrl, scope) => [ }), field: 'update_timestamp', sortable: true, - render: timestamp => formatDateTimeLocal(timestamp), + render: timestamp => formatDateTimeLocal(timestamp, timezone), }, { name: i18n.translate('xpack.monitoring.alerts.triggeredColumnTitle', { @@ -151,11 +152,14 @@ export const Alerts = ({ alerts, angular, sorting, pagination, onTableChange }) category: alert.metadata.link, })); + const injector = chrome.dangerouslyGetActiveInjector(); + const timezone = injector.get('config').get('dateFormat:tz'); + return ( <EuiMonitoringTable className="alertsTable" rows={alertsFlattened} - columns={getColumns(angular.kbnUrl, angular.scope)} + columns={getColumns(angular.kbnUrl, angular.scope, timezone)} sorting={{ ...sorting, sort: { diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js b/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js index 6f26abeadb3a07..661d51e068201f 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js +++ b/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js @@ -9,7 +9,7 @@ import { merge } from 'lodash'; import { CHART_LINE_COLOR, CHART_TEXT_COLOR } from '../../../common/constants'; export async function getChartOptions(axisOptions) { - const $injector = await chrome.dangerouslyGetActiveInjector(); + const $injector = chrome.dangerouslyGetActiveInjector(); const timezone = $injector.get('config').get('dateFormat:tz'); const opts = { legend: { diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js index a8001638f4399d..8455fb8cf3088c 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js @@ -6,6 +6,7 @@ import React, { Fragment } from 'react'; import moment from 'moment-timezone'; +import chrome from '../../../np_imports/ui/chrome'; import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert'; import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; @@ -57,6 +58,9 @@ export function AlertsPanel({ alerts, changeUrl }) { severityIcon.iconType = 'check'; } + const injector = chrome.dangerouslyGetActiveInjector(); + const timezone = injector.get('config').get('dateFormat:tz'); + return ( <EuiCallOut key={`alert-item-${index}`} @@ -79,7 +83,7 @@ export function AlertsPanel({ alerts, changeUrl }) { id="xpack.monitoring.cluster.overview.alertsPanel.lastCheckedTimeText" defaultMessage="Last checked {updateDateTime} (triggered {duration} ago)" values={{ - updateDateTime: formatDateTimeLocal(item.update_timestamp), + updateDateTime: formatDateTimeLocal(item.update_timestamp, timezone), duration: formatTimestampToDuration(item.timestamp, CALCULATE_DURATION_SINCE), }} /> diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap index 366c23135cc76b..e55f9c84b51fe8 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap @@ -147,7 +147,7 @@ exports[`CcrShard that it renders normally 1`] = ` size="s" > <h2> - September 27, 2018 9:32:09 AM + September 27, 2018 1:32:09 PM </h2> </EuiTitle> <EuiHorizontalRule /> diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js index 68c87b386da499..af0ff323b7ba87 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js @@ -5,6 +5,7 @@ */ import React, { Fragment, PureComponent } from 'react'; +import chrome from '../../../np_imports/ui/chrome'; import { EuiPage, EuiPageBody, @@ -92,6 +93,8 @@ export class CcrShard extends PureComponent { renderLatestStat() { const { stat, timestamp } = this.props; + const injector = chrome.dangerouslyGetActiveInjector(); + const timezone = injector.get('config').get('dateFormat:tz'); return ( <EuiAccordion @@ -110,7 +113,7 @@ export class CcrShard extends PureComponent { > <Fragment> <EuiTitle size="s"> - <h2>{formatDateTimeLocal(timestamp)}</h2> + <h2>{formatDateTimeLocal(timestamp, timezone)}</h2> </EuiTitle> <EuiHorizontalRule /> <EuiCodeBlock language="json">{JSON.stringify(stat, null, 2)}</EuiCodeBlock> diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js index 17caa8429a2750..b950c2ca0a6d28 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js @@ -11,6 +11,7 @@ import { CcrShard } from './ccr_shard'; jest.mock('../../../np_imports/ui/chrome', () => { return { getBasePath: () => '', + dangerouslyGetActiveInjector: () => ({ get: () => ({ get: () => 'utc' }) }), }; }); diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js index 692025631f3b8b..133b520947b1b1 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import chrome from '../../../np_imports/ui/chrome'; import { capitalize } from 'lodash'; import { formatMetric } from 'plugins/monitoring/lib/format_number'; import { formatDateTimeLocal } from '../../../../common/formatting'; @@ -38,13 +39,15 @@ export const parseProps = props => { } = props; const { files, size } = index; + const injector = chrome.dangerouslyGetActiveInjector(); + const timezone = injector.get('config').get('dateFormat:tz'); return { name: indexName || index.name, shard: `${id} / ${isPrimary ? 'Primary' : 'Replica'}`, relocationType: type === 'PRIMARY_RELOCATION' ? 'Primary Relocation' : normalizeString(type), stage: normalizeString(stage), - startTime: formatDateTimeLocal(startTimeInMillis), + startTime: formatDateTimeLocal(startTimeInMillis, timezone), totalTime: formatMetric(Math.floor(totalTimeInMillis / 1000), '00:00:00'), isCopiedFromPrimary: !isPrimary || type === 'PRIMARY_RELOCATION', sourceName: source.name === undefined ? 'n/a' : source.name, diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js b/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js index 926f5cdda26a74..744ebb5a7ceb4d 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js +++ b/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js @@ -14,6 +14,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Reason } from './reason'; import { capabilities } from '../../np_imports/ui/capabilities'; +const getFormattedDateTimeLocal = timestamp => { + const injector = chrome.dangerouslyGetActiveInjector(); + const timezone = injector.get('config').get('dateFormat:tz'); + return formatDateTimeLocal(timestamp, timezone); +}; + const columnTimestampTitle = i18n.translate('xpack.monitoring.logs.listing.timestampTitle', { defaultMessage: 'Timestamp', }); @@ -43,7 +49,7 @@ const columns = [ field: 'timestamp', name: columnTimestampTitle, width: '12%', - render: timestamp => formatDateTimeLocal(timestamp, true), + render: timestamp => getFormattedDateTimeLocal(timestamp), }, { field: 'level', @@ -73,7 +79,7 @@ const clusterColumns = [ field: 'timestamp', name: columnTimestampTitle, width: '12%', - render: timestamp => formatDateTimeLocal(timestamp, true), + render: timestamp => getFormattedDateTimeLocal(timestamp), }, { field: 'level', diff --git a/x-pack/legacy/plugins/monitoring/public/views/license/controller.js b/x-pack/legacy/plugins/monitoring/public/views/license/controller.js index dcd3ca76ceffd1..ce6e9c8fb74cd7 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/license/controller.js +++ b/x-pack/legacy/plugins/monitoring/public/views/license/controller.js @@ -54,11 +54,13 @@ export class LicenseViewController { } renderReact($scope) { + const injector = chrome.dangerouslyGetActiveInjector(); + const timezone = injector.get('config').get('dateFormat:tz'); $scope.$evalAsync(() => { const { isPrimaryCluster, license, isExpired, uploadLicensePath } = this; let expiryDate = license.expiry_date_in_millis; if (license.expiry_date_in_millis !== undefined) { - expiryDate = formatDateTimeLocal(license.expiry_date_in_millis); + expiryDate = formatDateTimeLocal(license.expiry_date_in_millis, timezone); } // Mount the React component to the template diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/histogram_configs.ts b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/histogram_configs.ts new file mode 100644 index 00000000000000..fbcf4c6ed039b6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/histogram_configs.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as i18n from './translations'; +import { MatrixHistogramOption, MatrixHisrogramConfigs } from '../matrix_histogram/types'; +import { HistogramType } from '../../graphql/types'; + +export const alertsStackByOptions: MatrixHistogramOption[] = [ + { + text: 'event.category', + value: 'event.category', + }, + { + text: 'event.module', + value: 'event.module', + }, +]; + +const DEFAULT_STACK_BY = 'event.module'; + +export const histogramConfigs: MatrixHisrogramConfigs = { + defaultStackByOption: + alertsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[1], + errorMessage: i18n.ERROR_FETCHING_ALERTS_DATA, + histogramType: HistogramType.alerts, + stackByOptions: alertsStackByOptions, + subtitle: undefined, + title: i18n.ALERTS_GRAPH_TITLE, +}; diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx index a8c2f429040ea6..587002c24d5269 100644 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx @@ -3,30 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { noop } from 'lodash/fp'; -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback, useMemo } from 'react'; import numeral from '@elastic/numeral'; import { AlertsComponentsQueryProps } from './types'; import { AlertsTable } from './alerts_table'; import * as i18n from './translations'; -import { MatrixHistogramOption } from '../matrix_histogram/types'; -import { MatrixHistogramContainer } from '../../containers/matrix_histogram'; -import { MatrixHistogramGqlQuery } from '../../containers/matrix_histogram/index.gql_query'; import { useUiSetting$ } from '../../lib/kibana'; import { DEFAULT_NUMBER_FORMAT } from '../../../common/constants'; +import { MatrixHistogramContainer } from '../matrix_histogram'; +import { histogramConfigs } from './histogram_configs'; +import { MatrixHisrogramConfigs } from '../matrix_histogram/types'; const ID = 'alertsOverTimeQuery'; -export const alertsStackByOptions: MatrixHistogramOption[] = [ - { - text: 'event.category', - value: 'event.category', - }, - { - text: 'event.module', - value: 'event.module', - }, -]; -const dataKey = 'AlertsHistogram'; export const AlertsView = ({ deleteQuery, @@ -34,21 +22,10 @@ export const AlertsView = ({ filterQuery, pageFilters, setQuery, - skip, startDate, type, - updateDateRange = noop, }: AlertsComponentsQueryProps) => { const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, []); - const getSubtitle = useCallback( (totalCount: number) => `${i18n.SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${i18n.UNIT( @@ -56,27 +33,32 @@ export const AlertsView = ({ )}`, [] ); + const alertsHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + subtitle: getSubtitle, + }), + [getSubtitle] + ); + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, [deleteQuery]); return ( <> <MatrixHistogramContainer - dataKey={dataKey} - defaultStackByOption={alertsStackByOptions[1]} endDate={endDate} - errorMessage={i18n.ERROR_FETCHING_ALERTS_DATA} filterQuery={filterQuery} id={ID} - isAlertsHistogram={true} - query={MatrixHistogramGqlQuery} setQuery={setQuery} - skip={skip} sourceId="default" - stackByOptions={alertsStackByOptions} startDate={startDate} - subtitle={getSubtitle} - title={i18n.ALERTS_GRAPH_TITLE} type={type} - updateDateRange={updateDateRange} + {...alertsHistogramConfigs} /> <AlertsTable endDate={endDate} startDate={startDate} pageFilters={pageFilters} /> </> diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts index e6d6fdf273ec86..a24c66e31e6708 100644 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts @@ -13,14 +13,7 @@ type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps; export interface AlertsComponentsQueryProps extends Pick< CommonQueryProps, - | 'deleteQuery' - | 'endDate' - | 'filterQuery' - | 'skip' - | 'setQuery' - | 'startDate' - | 'type' - | 'updateDateRange' + 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' | 'type' > { pageFilters: Filter[]; stackByOptions?: MatrixHistogramOption[]; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx index 62f1ac56890ca1..03b412f5756466 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx @@ -28,10 +28,10 @@ const chartDefaultRendering: Rendering = 'canvas'; export type UpdateDateRange = (min: number, max: number) => void; export interface ChartData { - x: number | string | null; - y: number | string | null; + x?: number | string | null; + y?: number | string | null; y0?: number; - g?: number | string; + g?: number | string | null; } export interface ChartSeriesConfigs { diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000000..0e518e48e2e888 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"<div class=\\"sc-AykKF jbBKkl\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKH iNPult\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"barchart\\"></div></div></div><div class=\\"euiSpacer euiSpacer--l\\"></div>"`; + +exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"<div class=\\"sc-AykKF hneqJM\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKH iNPult\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"matrixLoader\\"></div></div></div><div class=\\"euiSpacer euiSpacer--l\\"></div>"`; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx index a44efed47372db..db5b1f7f03ee3d 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx @@ -6,17 +6,19 @@ /* eslint-disable react/display-name */ -import { shallow } from 'enzyme'; +import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; import { MatrixHistogram } from '.'; -import { MatrixHistogramGqlQuery as mockQuery } from '../../containers/matrix_histogram/index.gql_query'; - +import { useQuery } from '../../containers/matrix_histogram'; +import { HistogramType } from '../../graphql/types'; jest.mock('../../lib/kibana'); -jest.mock('../loader', () => { +jest.mock('./matrix_loader', () => { return { - Loader: () => <div className="loader" />, + MatrixLoader: () => { + return <div className="matrixLoader" />; + }, }; }); @@ -32,17 +34,31 @@ jest.mock('../charts/barchart', () => { }; }); +jest.mock('../../containers/matrix_histogram', () => { + return { + useQuery: jest.fn(), + }; +}); + +jest.mock('../../components/matrix_histogram/utils', () => { + return { + getBarchartConfigs: jest.fn(), + getCustomChartData: jest.fn().mockReturnValue(true), + }; +}); + describe('Matrix Histogram Component', () => { + let wrapper: ReactWrapper; + const mockMatrixOverTimeHistogramProps = { - dataKey: 'mockDataKey', defaultIndex: ['defaultIndex'], defaultStackByOption: { text: 'text', value: 'value' }, endDate: new Date('2019-07-18T20:00:00.000Z').valueOf(), errorMessage: 'error', + histogramType: HistogramType.alerts, id: 'mockId', isInspected: false, isPtrIncluded: false, - query: mockQuery, setQuery: jest.fn(), skip: false, sourceId: 'default', @@ -52,36 +68,56 @@ describe('Matrix Histogram Component', () => { subtitle: 'mockSubtitle', totalCount: -1, title: 'mockTitle', - updateDateRange: jest.fn(), + dispatchSetAbsoluteRangeDatePicker: jest.fn(), }; - describe('rendering', () => { - test('it renders EuiLoadingContent on initialLoad', () => { - const wrapper = shallow(<MatrixHistogram {...mockMatrixOverTimeHistogramProps} />); - expect(wrapper.find(`[data-test-subj="initialLoadingPanelMatrixOverTime"]`)).toBeTruthy(); + beforeAll(() => { + (useQuery as jest.Mock).mockReturnValue({ + data: null, + loading: false, + inspect: false, + totalCount: null, }); - - test('it renders Loader while fetching data if visited before', () => { - const mockProps = { - ...mockMatrixOverTimeHistogramProps, - data: [{ x: new Date('2019-09-16T02:20:00.000Z').valueOf(), y: 3787, g: 'config_change' }], - totalCount: 10, - loading: true, - }; - const wrapper = shallow(<MatrixHistogram {...mockProps} />); - expect(wrapper.find('.loader')).toBeTruthy(); + wrapper = mount(<MatrixHistogram {...mockMatrixOverTimeHistogramProps} />); + }); + describe('on initial load', () => { + test('it renders MatrixLoader', () => { + expect(wrapper.html()).toMatchSnapshot(); + expect(wrapper.find('MatrixLoader').exists()).toBe(true); }); + }); - test('it renders BarChart if data available', () => { - const mockProps = { - ...mockMatrixOverTimeHistogramProps, - data: [{ x: new Date('2019-09-16T02:20:00.000Z').valueOf(), y: 3787, g: 'config_change' }], - totalCount: 10, + describe('not initial load', () => { + beforeAll(() => { + (useQuery as jest.Mock).mockReturnValue({ + data: [ + { x: 1, y: 2, g: 'g1' }, + { x: 2, y: 4, g: 'g1' }, + { x: 3, y: 6, g: 'g1' }, + { x: 1, y: 1, g: 'g2' }, + { x: 2, y: 3, g: 'g2' }, + { x: 3, y: 5, g: 'g2' }, + ], loading: false, - }; - const wrapper = shallow(<MatrixHistogram {...mockProps} />); + inspect: false, + totalCount: 1, + }); + wrapper.setProps({ endDate: 100 }); + wrapper.update(); + }); + test('it renders no MatrixLoader', () => { + expect(wrapper.html()).toMatchSnapshot(); + expect(wrapper.find(`MatrixLoader`).exists()).toBe(false); + }); + + test('it shows BarChart if data available', () => { + expect(wrapper.find(`.barchart`).exists()).toBe(true); + }); + }); - expect(wrapper.find(`.barchart`)).toBeTruthy(); + describe('select dropdown', () => { + test('should be hidden if only one option is provided', () => { + expect(wrapper.find('EuiSelect').exists()).toBe(false); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx index 04b988f8270f37..cb9afde899cf80 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx @@ -4,19 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect, useCallback } from 'react'; -import { ScaleType } from '@elastic/charts'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { Position } from '@elastic/charts'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSelect, EuiSpacer } from '@elastic/eui'; import { noop } from 'lodash/fp'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; import * as i18n from './translations'; import { BarChart } from '../charts/barchart'; import { HeaderSection } from '../header_section'; import { MatrixLoader } from './matrix_loader'; import { Panel } from '../panel'; -import { getBarchartConfigs, getCustomChartData } from './utils'; -import { useQuery } from '../../containers/matrix_histogram/utils'; +import { getBarchartConfigs, getCustomChartData } from '../../components/matrix_histogram/utils'; +import { useQuery } from '../../containers/matrix_histogram'; import { MatrixHistogramProps, MatrixHistogramOption, @@ -26,6 +28,35 @@ import { import { ChartSeriesData } from '../charts/common'; import { InspectButtonContainer } from '../inspect'; +import { State, inputsSelectors, hostsModel, networkModel } from '../../store'; + +import { + MatrixHistogramMappingTypes, + GetTitle, + GetSubTitle, +} from '../../components/matrix_histogram/types'; +import { SetQuery } from '../../pages/hosts/navigation/types'; +import { QueryTemplateProps } from '../../containers/query_template'; +import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { HistogramType } from '../../graphql/types'; + +export interface OwnProps extends QueryTemplateProps { + defaultStackByOption: MatrixHistogramOption; + errorMessage: string; + headerChildren?: React.ReactNode; + hideHistogramIfEmpty?: boolean; + histogramType: HistogramType; + id: string; + legendPosition?: Position; + mapping?: MatrixHistogramMappingTypes; + setQuery: SetQuery; + showLegend?: boolean; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title: string | GetTitle; + type: hostsModel.HostsType | networkModel.NetworkType; +} + const DEFAULT_PANEL_HEIGHT = 300; const HeaderChildrenFlexItem = styled(EuiFlexItem)` @@ -41,45 +72,50 @@ const HistogramPanel = styled(Panel)<{ height?: number }>` export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & MatrixHistogramQueryProps> = ({ chartHeight, - dataKey, defaultStackByOption, endDate, errorMessage, filterQuery, headerChildren, + histogramType, hideHistogramIfEmpty = false, id, - isAlertsHistogram, - isAnomaliesHistogram, - isAuthenticationsHistogram, - isDnsHistogram, - isEventsHistogram, isInspected, - legendPosition = 'right', + legendPosition, mapping, panelHeight = DEFAULT_PANEL_HEIGHT, - query, - scaleType = ScaleType.Time, setQuery, - showLegend = true, - skip, + showLegend, stackByOptions, startDate, subtitle, title, - updateDateRange, + dispatchSetAbsoluteRangeDatePicker, yTickFormatter, }) => { - const barchartConfigs = getBarchartConfigs({ - chartHeight, - from: startDate, - legendPosition, - to: endDate, - onBrushEnd: updateDateRange, - scaleType, - yTickFormatter, - showLegend, - }); + const barchartConfigs = useMemo( + () => + getBarchartConfigs({ + chartHeight, + from: startDate, + legendPosition, + to: endDate, + onBrushEnd: (min: number, max: number) => { + dispatchSetAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + yTickFormatter, + showLegend, + }), + [ + chartHeight, + startDate, + legendPosition, + endDate, + dispatchSetAbsoluteRangeDatePicker, + yTickFormatter, + showLegend, + ] + ); const [isInitialLoading, setIsInitialLoading] = useState(true); const [selectedStackByOption, setSelectedStackByOption] = useState<MatrixHistogramOption>( defaultStackByOption @@ -100,19 +136,11 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & const { data, loading, inspect, totalCount, refetch = noop } = useQuery<{}, HistogramAggregation>( { - dataKey, endDate, errorMessage, filterQuery, - query, - skip, + histogramType, startDate, - title, - isAlertsHistogram, - isAnomaliesHistogram, - isAuthenticationsHistogram, - isDnsHistogram, - isEventsHistogram, isInspected, stackByField: selectedStackByOption.value, } @@ -129,7 +157,6 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & } else { setHideHistogram(false); } - setBarChartData(getCustomChartData(data, mapping)); setQuery({ id, inspect, loading, refetch }); @@ -145,8 +172,11 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & setQuery, hideHistogramIfEmpty, totalCount, + id, + inspect, isInspected, loading, + refetch, data, refetch, isInitialLoading, @@ -174,7 +204,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & <HeaderSection id={id} title={titleWithStackByField} - subtitle={!loading && (totalCount >= 0 ? subtitleWithCounts : null)} + subtitle={!isInitialLoading && (totalCount >= 0 ? subtitleWithCounts : null)} > <EuiFlexGroup alignItems="center" gutterSize="none"> <EuiFlexItem grow={false}> @@ -197,7 +227,10 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & <HeaderSection id={id} title={titleWithStackByField} - subtitle={!isInitialLoading && (totalCount >= 0 ? subtitleWithCounts : null)} + subtitle={ + !isInitialLoading && + (totalCount != null && totalCount >= 0 ? subtitleWithCounts : null) + } > <EuiFlexGroup alignItems="center" gutterSize="none"> <EuiFlexItem grow={false}> @@ -224,3 +257,20 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & }; export const MatrixHistogram = React.memo(MatrixHistogramComponent); + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { type, id }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +export const MatrixHistogramContainer = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps, { + dispatchSetAbsoluteRangeDatePicker: setAbsoluteRangeDatePicker, + }) +)(MatrixHistogram); diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts index 88f8f1ff28fa92..fda4f5d15d95c7 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts @@ -4,20 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ScaleType, Position } from '@elastic/charts'; -import { SetStateAction } from 'react'; -import { DocumentNode } from 'graphql'; -import { - MatrixOverTimeHistogramData, - MatrixOverOrdinalHistogramData, - NetworkDnsSortField, - PaginationInputPaginated, -} from '../../graphql/types'; -import { UpdateDateRange } from '../charts/common'; +import { ScaleType, Position, TickFormatter } from '@elastic/charts'; +import { ActionCreator } from 'redux'; import { ESQuery } from '../../../common/typed_json'; import { SetQuery } from '../../pages/hosts/navigation/types'; +import { InputsModelId } from '../../store/inputs/constants'; +import { HistogramType } from '../../graphql/types'; +import { UpdateDateRange } from '../charts/common'; -export type MatrixHistogramDataTypes = MatrixOverTimeHistogramData | MatrixOverOrdinalHistogramData; export type MatrixHistogramMappingTypes = Record< string, { key: string; value: null; color?: string | undefined } @@ -30,10 +24,27 @@ export interface MatrixHistogramOption { export type GetSubTitle = (count: number) => string; export type GetTitle = (matrixHistogramOption: MatrixHistogramOption) => string; -export interface MatrixHistogramBasicProps { +export interface MatrixHisrogramConfigs { + defaultStackByOption: MatrixHistogramOption; + errorMessage: string; + hideHistogramIfEmpty?: boolean; + histogramType: HistogramType; + legendPosition?: Position; + mapping?: MatrixHistogramMappingTypes; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title: string | GetTitle; +} + +interface MatrixHistogramBasicProps { chartHeight?: number; defaultIndex: string[]; defaultStackByOption: MatrixHistogramOption; + dispatchSetAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; endDate: number; headerChildren?: React.ReactNode; hideHistogramIfEmpty?: boolean; @@ -42,35 +53,20 @@ export interface MatrixHistogramBasicProps { mapping?: MatrixHistogramMappingTypes; panelHeight?: number; setQuery: SetQuery; - sourceId: string; startDate: number; stackByOptions: MatrixHistogramOption[]; subtitle?: string | GetSubTitle; - title?: string; - updateDateRange: UpdateDateRange; + title?: string | GetTitle; } export interface MatrixHistogramQueryProps { - activePage?: number; - dataKey: string; endDate: number; errorMessage: string; filterQuery?: ESQuery | string | undefined; - limit?: number; - query: DocumentNode; - sort?: NetworkDnsSortField; stackByField: string; - skip: boolean; startDate: number; - title: string | GetTitle; - isAlertsHistogram?: boolean; - isAnomaliesHistogram?: boolean; - isAuthenticationsHistogram?: boolean; - isDnsHistogram?: boolean; - isEventsHistogram?: boolean; isInspected: boolean; - isPtrIncluded?: boolean; - pagination?: PaginationInputPaginated; + histogramType: HistogramType; } export interface MatrixHistogramProps extends MatrixHistogramBasicProps { @@ -98,31 +94,38 @@ export interface HistogramAggregation { }; } -export interface SignalsResponse { - took: number; - timeout: boolean; -} - -export interface SignalSearchResponse<Hit = {}, Aggregations = {} | undefined> - extends SignalsResponse { - _shards: { - total: number; - successful: number; - skipped: number; - failed: number; +export interface BarchartConfigs { + series: { + xScaleType: ScaleType; + yScaleType: ScaleType; + stackAccessors: string[]; }; - aggregations?: Aggregations; - hits: { - total: { - value: number; - relation: string; + axis: { + xTickFormatter: TickFormatter; + yTickFormatter: TickFormatter; + tickSize: number; + }; + settings: { + legendPosition: Position; + onBrushEnd: UpdateDateRange; + showLegend: boolean; + theme: { + scales: { + barsPadding: number; + }; + chartMargins: { + left: number; + right: number; + top: number; + bottom: number; + }; + chartPaddings: { + left: number; + right: number; + top: number; + bottom: number; + }; }; - hits: Hit[]; }; + customHeight: number; } - -export type Return<Hit, Aggs> = [ - boolean, - SignalSearchResponse<Hit, Aggs> | null, - React.Dispatch<SetStateAction<string>> -]; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.test.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.test.ts new file mode 100644 index 00000000000000..2c34a307bfdedd --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.test.ts @@ -0,0 +1,139 @@ +/* + * 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 { + getBarchartConfigs, + DEFAULT_CHART_HEIGHT, + DEFAULT_Y_TICK_FORMATTER, + formatToChartDataItem, + getCustomChartData, +} from './utils'; +import { UpdateDateRange } from '../charts/common'; +import { Position } from '@elastic/charts'; +import { MatrixOverTimeHistogramData } from '../../graphql/types'; +import { BarchartConfigs } from './types'; + +describe('utils', () => { + describe('getBarchartConfigs', () => { + describe('it should get correct default values', () => { + let configs: BarchartConfigs; + beforeAll(() => { + configs = getBarchartConfigs({ + from: 0, + to: 0, + onBrushEnd: jest.fn() as UpdateDateRange, + }); + }); + + test('it should set default chartHeight', () => { + expect(configs.customHeight).toEqual(DEFAULT_CHART_HEIGHT); + }); + + test('it should show legend by default', () => { + expect(configs.settings.showLegend).toEqual(true); + }); + + test('it should put legend on the right', () => { + expect(configs.settings.legendPosition).toEqual(Position.Right); + }); + + test('it should format Y tick to local string', () => { + expect(configs.axis.yTickFormatter).toEqual(DEFAULT_Y_TICK_FORMATTER); + }); + }); + + describe('it should set custom configs', () => { + let configs: BarchartConfigs; + const mockYTickFormatter = jest.fn(); + const mockChartHeight = 100; + + beforeAll(() => { + configs = getBarchartConfigs({ + chartHeight: mockChartHeight, + from: 0, + to: 0, + onBrushEnd: jest.fn() as UpdateDateRange, + yTickFormatter: mockYTickFormatter, + showLegend: false, + }); + }); + + test('it should set custom chart height', () => { + expect(configs.customHeight).toEqual(mockChartHeight); + }); + + test('it should hide legend', () => { + expect(configs.settings.showLegend).toEqual(false); + }); + + test('it should format y tick with custom formatter', () => { + expect(configs.axis.yTickFormatter).toEqual(mockYTickFormatter); + }); + }); + }); + + describe('formatToChartDataItem', () => { + test('it should format data correctly', () => { + const data: [string, MatrixOverTimeHistogramData[]] = [ + 'g1', + [ + { x: 1, y: 2, g: 'g1' }, + { x: 2, y: 4, g: 'g1' }, + { x: 3, y: 6, g: 'g1' }, + ], + ]; + const result = formatToChartDataItem(data); + expect(result).toEqual({ + key: 'g1', + value: [ + { x: 1, y: 2, g: 'g1' }, + { x: 2, y: 4, g: 'g1' }, + { x: 3, y: 6, g: 'g1' }, + ], + }); + }); + }); + + describe('getCustomChartData', () => { + test('should handle the case when no data provided', () => { + const data = null; + const result = getCustomChartData(data); + + expect(result).toEqual([]); + }); + + test('shoule format data correctly', () => { + const data = [ + { x: 1, y: 2, g: 'g1' }, + { x: 2, y: 4, g: 'g1' }, + { x: 3, y: 6, g: 'g1' }, + { x: 1, y: 1, g: 'g2' }, + { x: 2, y: 3, g: 'g2' }, + { x: 3, y: 5, g: 'g2' }, + ]; + const result = getCustomChartData(data); + + expect(result).toEqual([ + { + key: 'g1', + value: [ + { x: 1, y: 2, g: 'g1' }, + { x: 2, y: 4, g: 'g1' }, + { x: 3, y: 6, g: 'g1' }, + ], + }, + { + key: 'g2', + value: [ + { x: 1, y: 1, g: 'g2' }, + { x: 2, y: 3, g: 'g2' }, + { x: 3, y: 5, g: 'g2' }, + ], + }, + ]); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts index 95b1cd806cf6cb..ccd1b03eb54741 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts @@ -7,7 +7,8 @@ import { ScaleType, Position } from '@elastic/charts'; import { get, groupBy, map, toPairs } from 'lodash/fp'; import { UpdateDateRange, ChartSeriesData } from '../charts/common'; -import { MatrixHistogramDataTypes, MatrixHistogramMappingTypes } from './types'; +import { MatrixHistogramMappingTypes, BarchartConfigs } from './types'; +import { MatrixOverTimeHistogramData } from '../../graphql/types'; import { histogramDateTimeFormatter } from '../utils'; interface GetBarchartConfigsProps { @@ -15,40 +16,35 @@ interface GetBarchartConfigsProps { from: number; legendPosition?: Position; to: number; - scaleType: ScaleType; onBrushEnd: UpdateDateRange; yTickFormatter?: (value: number) => string; showLegend?: boolean; } export const DEFAULT_CHART_HEIGHT = 174; +export const DEFAULT_Y_TICK_FORMATTER = (value: string | number): string => value.toLocaleString(); export const getBarchartConfigs = ({ chartHeight, from, legendPosition, to, - scaleType, onBrushEnd, yTickFormatter, showLegend, -}: GetBarchartConfigsProps) => ({ +}: GetBarchartConfigsProps): BarchartConfigs => ({ series: { - xScaleType: scaleType || ScaleType.Time, + xScaleType: ScaleType.Time, yScaleType: ScaleType.Linear, stackAccessors: ['g'], }, axis: { - xTickFormatter: - scaleType === ScaleType.Time ? histogramDateTimeFormatter([from, to]) : undefined, - yTickFormatter: - yTickFormatter != null - ? yTickFormatter - : (value: string | number): string => value.toLocaleString(), + xTickFormatter: histogramDateTimeFormatter([from, to]), + yTickFormatter: yTickFormatter != null ? yTickFormatter : DEFAULT_Y_TICK_FORMATTER, tickSize: 8, }, settings: { - legendPosition: legendPosition ?? Position.Bottom, + legendPosition: legendPosition ?? Position.Right, onBrushEnd, showLegend: showLegend ?? true, theme: { @@ -74,14 +70,14 @@ export const getBarchartConfigs = ({ export const formatToChartDataItem = ([key, value]: [ string, - MatrixHistogramDataTypes[] + MatrixOverTimeHistogramData[] ]): ChartSeriesData => ({ key, value, }); export const getCustomChartData = ( - data: MatrixHistogramDataTypes[] | null, + data: MatrixOverTimeHistogramData[] | null, mapping?: MatrixHistogramMappingTypes ): ChartSeriesData[] => { if (!data) return []; @@ -92,7 +88,7 @@ export const getCustomChartData = ( if (mapping) return map((item: ChartSeriesData) => { const mapItem = get(item.key, mapping); - return { ...item, color: mapItem.color }; + return { ...item, color: mapItem?.color }; }, formattedChartData); else return formattedChartData; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts new file mode 100644 index 00000000000000..f63349d3e573ad --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as i18n from './translations'; +import { + MatrixHistogramOption, + MatrixHisrogramConfigs, +} from '../../../components/matrix_histogram/types'; +import { HistogramType } from '../../../graphql/types'; + +export const anomaliesStackByOptions: MatrixHistogramOption[] = [ + { + text: i18n.ANOMALIES_STACK_BY_JOB_ID, + value: 'job_id', + }, +]; + +const DEFAULT_STACK_BY = i18n.ANOMALIES_STACK_BY_JOB_ID; + +export const histogramConfigs: MatrixHisrogramConfigs = { + defaultStackByOption: + anomaliesStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? anomaliesStackByOptions[0], + errorMessage: i18n.ERROR_FETCHING_ANOMALIES_DATA, + hideHistogramIfEmpty: true, + histogramType: HistogramType.anomalies, + stackByOptions: anomaliesStackByOptions, + subtitle: undefined, + title: i18n.ANOMALIES_TITLE, +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx index e34832aa88c930..85e19248f2eb52 100644 --- a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx @@ -5,23 +5,14 @@ */ import React, { useEffect } from 'react'; -import * as i18n from './translations'; import { AnomaliesQueryTabBodyProps } from './types'; import { getAnomaliesFilterQuery } from './utils'; import { useSiemJobs } from '../../../components/ml_popover/hooks/use_siem_jobs'; import { useUiSetting$ } from '../../../lib/kibana'; import { DEFAULT_ANOMALY_SCORE } from '../../../../common/constants'; -import { MatrixHistogramContainer } from '../../matrix_histogram'; -import { MatrixHistogramOption } from '../../../components/matrix_histogram/types'; -import { MatrixHistogramGqlQuery } from '../../matrix_histogram/index.gql_query'; - +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { histogramConfigs } from './histogram_configs'; const ID = 'anomaliesOverTimeQuery'; -const anomaliesStackByOptions: MatrixHistogramOption[] = [ - { - text: i18n.ANOMALIES_STACK_BY_JOB_ID, - value: 'job_id', - }, -]; export const AnomaliesQueryTabBody = ({ deleteQuery, @@ -33,7 +24,6 @@ export const AnomaliesQueryTabBody = ({ narrowDateRange, filterQuery, anomaliesFilterQuery, - updateDateRange = () => {}, AnomaliesTableComponent, flowTarget, ip, @@ -61,23 +51,14 @@ export const AnomaliesQueryTabBody = ({ return ( <> <MatrixHistogramContainer - isAnomaliesHistogram={true} - dataKey="AnomaliesHistogram" - defaultStackByOption={anomaliesStackByOptions[0]} endDate={endDate} - errorMessage={i18n.ERROR_FETCHING_ANOMALIES_DATA} filterQuery={mergedFilterQuery} - hideHistogramIfEmpty={true} id={ID} - query={MatrixHistogramGqlQuery} setQuery={setQuery} - skip={skip} sourceId="default" - stackByOptions={anomaliesStackByOptions} startDate={startDate} - title={i18n.ANOMALIES_TITLE} type={type} - updateDateRange={updateDateRange} + {...histogramConfigs} /> <AnomaliesTableComponent startDate={startDate} diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts index e21d4c6e34ff86..6fb729ca7e9a0d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts @@ -8,94 +8,23 @@ import gql from 'graphql-tag'; export const MatrixHistogramGqlQuery = gql` query GetMatrixHistogramQuery( - $isAlertsHistogram: Boolean! - $isAnomaliesHistogram: Boolean! - $isAuthenticationsHistogram: Boolean! - $isDnsHistogram: Boolean! $defaultIndex: [String!]! - $isEventsHistogram: Boolean! $filterQuery: String + $histogramType: HistogramType! $inspect: Boolean! $sourceId: ID! - $stackByField: String + $stackByField: String! $timerange: TimerangeInput! ) { source(id: $sourceId) { id - AlertsHistogram( + MatrixHistogram( timerange: $timerange filterQuery: $filterQuery defaultIndex: $defaultIndex stackByField: $stackByField - ) @include(if: $isAlertsHistogram) { - matrixHistogramData { - x - y - g - } - totalCount - inspect @include(if: $inspect) { - dsl - response - } - } - AnomaliesHistogram( - timerange: $timerange - filterQuery: $filterQuery - defaultIndex: $defaultIndex - stackByField: $stackByField - ) @include(if: $isAnomaliesHistogram) { - matrixHistogramData { - x - y - g - } - totalCount - inspect @include(if: $inspect) { - dsl - response - } - } - AuthenticationsHistogram( - timerange: $timerange - filterQuery: $filterQuery - defaultIndex: $defaultIndex - stackByField: $stackByField - ) @include(if: $isAuthenticationsHistogram) { - matrixHistogramData { - x - y - g - } - totalCount - inspect @include(if: $inspect) { - dsl - response - } - } - EventsHistogram( - timerange: $timerange - filterQuery: $filterQuery - defaultIndex: $defaultIndex - stackByField: $stackByField - ) @include(if: $isEventsHistogram) { - matrixHistogramData { - x - y - g - } - totalCount - inspect @include(if: $inspect) { - dsl - response - } - } - NetworkDnsHistogram( - timerange: $timerange - filterQuery: $filterQuery - defaultIndex: $defaultIndex - stackByField: $stackByField - ) @include(if: $isDnsHistogram) { + histogramType: $histogramType + ) { matrixHistogramData { x y diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.test.tsx b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.test.tsx new file mode 100644 index 00000000000000..06367ab8657a87 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.test.tsx @@ -0,0 +1,154 @@ +/* + * 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 { useQuery } from '.'; +import { mount } from 'enzyme'; +import React from 'react'; +import { useApolloClient } from '../../utils/apollo_context'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import { MatrixOverTimeHistogramData, HistogramType } from '../../graphql/types'; +import { InspectQuery, Refetch } from '../../store/inputs/model'; + +const mockQuery = jest.fn().mockResolvedValue({ + data: { + source: { + MatrixHistogram: { + matrixHistogramData: [{}], + totalCount: 1, + inspect: false, + }, + }, + }, +}); + +const mockRejectQuery = jest.fn().mockRejectedValue(new Error()); +jest.mock('../../utils/apollo_context', () => ({ + useApolloClient: jest.fn(), +})); + +jest.mock('../../lib/kibana', () => { + return { + useUiSetting$: jest.fn().mockReturnValue(['mockDefaultIndex']), + }; +}); + +jest.mock('./index.gql_query', () => { + return { + MatrixHistogramGqlQuery: 'mockGqlQuery', + }; +}); + +jest.mock('../../components/ml/api/error_to_toaster'); + +describe('useQuery', () => { + let result: { + data: MatrixOverTimeHistogramData[] | null; + loading: boolean; + inspect: InspectQuery | null; + totalCount: number; + refetch: Refetch | undefined; + }; + describe('happy path', () => { + beforeAll(() => { + (useApolloClient as jest.Mock).mockReturnValue({ + query: mockQuery, + }); + const TestComponent = () => { + result = useQuery({ + endDate: 100, + errorMessage: 'fakeErrorMsg', + filterQuery: '', + histogramType: HistogramType.alerts, + isInspected: false, + stackByField: 'fakeField', + startDate: 0, + }); + + return <div />; + }; + + mount(<TestComponent />); + }); + + test('should set variables', () => { + expect(mockQuery).toBeCalledWith({ + query: 'mockGqlQuery', + fetchPolicy: 'network-only', + variables: { + filterQuery: '', + sourceId: 'default', + timerange: { + interval: '12h', + from: 0, + to: 100, + }, + defaultIndex: 'mockDefaultIndex', + inspect: false, + stackByField: 'fakeField', + histogramType: 'alerts', + }, + context: { + fetchOptions: { + abortSignal: new AbortController().signal, + }, + }, + }); + }); + + test('should setData', () => { + expect(result.data).toEqual([{}]); + }); + + test('should set total count', () => { + expect(result.totalCount).toEqual(1); + }); + + test('should set inspect', () => { + expect(result.inspect).toEqual(false); + }); + }); + + describe('failure path', () => { + beforeAll(() => { + mockQuery.mockClear(); + (useApolloClient as jest.Mock).mockReset(); + (useApolloClient as jest.Mock).mockReturnValue({ + query: mockRejectQuery, + }); + const TestComponent = () => { + result = useQuery({ + endDate: 100, + errorMessage: 'fakeErrorMsg', + filterQuery: '', + histogramType: HistogramType.alerts, + isInspected: false, + stackByField: 'fakeField', + startDate: 0, + }); + + return <div />; + }; + + mount(<TestComponent />); + }); + + test('should setData', () => { + expect(result.data).toEqual(null); + }); + + test('should set total count', () => { + expect(result.totalCount).toEqual(-1); + }); + + test('should set inspect', () => { + expect(result.inspect).toEqual(null); + }); + + test('should set error to toster', () => { + expect(errorToToaster).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/utils.ts b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts similarity index 61% rename from x-pack/legacy/plugins/siem/public/containers/matrix_histogram/utils.ts rename to x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts index 1df1aec76627ce..683d5b68c305b4 100644 --- a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/utils.ts +++ b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts @@ -3,12 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; -import { useEffect, useRef, useState } from 'react'; -import { - MatrixHistogramDataTypes, - MatrixHistogramQueryProps, -} from '../../components/matrix_histogram/types'; +import { useEffect, useState, useRef } from 'react'; +import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { useStateToaster } from '../../components/toasters'; import { errorToToaster } from '../../components/ml/api/error_to_toaster'; @@ -16,20 +12,15 @@ import { useUiSetting$ } from '../../lib/kibana'; import { createFilter } from '../helpers'; import { useApolloClient } from '../../utils/apollo_context'; import { inputsModel } from '../../store'; -import { GetMatrixHistogramQuery } from '../../graphql/types'; +import { MatrixHistogramGqlQuery } from './index.gql_query'; +import { GetMatrixHistogramQuery, MatrixOverTimeHistogramData } from '../../graphql/types'; export const useQuery = <Hit, Aggs, TCache = object>({ - dataKey, endDate, errorMessage, filterQuery, - isAlertsHistogram = false, - isAnomaliesHistogram = false, - isAuthenticationsHistogram = false, - isEventsHistogram = false, - isDnsHistogram = false, + histogramType, isInspected, - query, stackByField, startDate, }: MatrixHistogramQueryProps) => { @@ -37,30 +28,25 @@ export const useQuery = <Hit, Aggs, TCache = object>({ const [, dispatchToaster] = useStateToaster(); const refetch = useRef<inputsModel.Refetch>(); const [loading, setLoading] = useState<boolean>(false); - const [data, setData] = useState<MatrixHistogramDataTypes[] | null>(null); + const [data, setData] = useState<MatrixOverTimeHistogramData[] | null>(null); const [inspect, setInspect] = useState<inputsModel.InspectQuery | null>(null); - const [totalCount, setTotalCount] = useState(-1); + const [totalCount, setTotalCount] = useState<number>(-1); const apolloClient = useApolloClient(); - const matrixHistogramVariables: GetMatrixHistogramQuery.Variables = { - filterQuery: createFilter(filterQuery), - sourceId: 'default', - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - defaultIndex, - inspect: isInspected, - stackByField, - isAlertsHistogram, - isAnomaliesHistogram, - isAuthenticationsHistogram, - isDnsHistogram, - isEventsHistogram, - }; - useEffect(() => { + const matrixHistogramVariables: GetMatrixHistogramQuery.Variables = { + filterQuery: createFilter(filterQuery), + sourceId: 'default', + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + defaultIndex, + inspect: isInspected, + stackByField, + histogramType, + }; let isSubscribed = true; const abortCtrl = new AbortController(); const abortSignal = abortCtrl.signal; @@ -70,7 +56,7 @@ export const useQuery = <Hit, Aggs, TCache = object>({ setLoading(true); return apolloClient .query<GetMatrixHistogramQuery.Query, GetMatrixHistogramQuery.Variables>({ - query, + query: MatrixHistogramGqlQuery, fetchPolicy: 'network-only', variables: matrixHistogramVariables, context: { @@ -82,13 +68,10 @@ export const useQuery = <Hit, Aggs, TCache = object>({ .then( result => { if (isSubscribed) { - const isDataKeyAnArray = Array.isArray(dataKey); - const rootDataKey = isDataKeyAnArray ? dataKey[0] : `${dataKey}`; - const histogramDataKey = isDataKeyAnArray ? dataKey[1] : `matrixHistogramData`; - const source = getOr({}, `data.source.${rootDataKey}`, result); - setData(getOr([], histogramDataKey, source)); - setTotalCount(getOr(-1, 'totalCount', source)); - setInspect(getOr(null, 'inspect', source)); + const source = result?.data?.source?.MatrixHistogram ?? {}; + setData(source?.matrixHistogramData ?? []); + setTotalCount(source?.totalCount ?? -1); + setInspect(source?.inspect ?? null); setLoading(false); } }, @@ -97,8 +80,8 @@ export const useQuery = <Hit, Aggs, TCache = object>({ setData(null); setTotalCount(-1); setInspect(null); - errorToToaster({ title: errorMessage, error, dispatchToaster }); setLoading(false); + errorToToaster({ title: errorMessage, error, dispatchToaster }); } } ); @@ -111,13 +94,14 @@ export const useQuery = <Hit, Aggs, TCache = object>({ }; }, [ defaultIndex, - query, + errorMessage, filterQuery, + histogramType, isInspected, - isDnsHistogram, stackByField, startDate, endDate, + data, ]); return { data, loading, inspect, totalCount, refetch: refetch.current }; diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.tsx deleted file mode 100644 index 9e0b1579a7b655..00000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.tsx +++ /dev/null @@ -1,66 +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 { Position } from '@elastic/charts'; -import React from 'react'; -import { compose } from 'redux'; - -import { connect } from 'react-redux'; -import { State, inputsSelectors, hostsModel, networkModel } from '../../store'; -import { QueryTemplateProps } from '../query_template'; - -import { Maybe } from '../../graphql/types'; -import { MatrixHistogram } from '../../components/matrix_histogram'; -import { - MatrixHistogramOption, - MatrixHistogramMappingTypes, - GetTitle, - GetSubTitle, -} from '../../components/matrix_histogram/types'; -import { UpdateDateRange } from '../../components/charts/common'; -import { SetQuery } from '../../pages/hosts/navigation/types'; - -export interface OwnProps extends QueryTemplateProps { - chartHeight?: number; - dataKey: string | string[]; - defaultStackByOption: MatrixHistogramOption; - errorMessage: string; - headerChildren?: React.ReactNode; - hideHistogramIfEmpty?: boolean; - isAlertsHistogram?: boolean; - isAnomaliesHistogram?: boolean; - isAuthenticationsHistogram?: boolean; - id: string; - isDnsHistogram?: boolean; - isEventsHistogram?: boolean; - legendPosition?: Position; - mapping?: MatrixHistogramMappingTypes; - panelHeight?: number; - query: Maybe<string>; - setQuery: SetQuery; - showLegend?: boolean; - sourceId: string; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - title: string | GetTitle; - type: hostsModel.HostsType | networkModel.NetworkType; - updateDateRange: UpdateDateRange; -} - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -export const MatrixHistogramContainer = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps) -)(MatrixHistogram); diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index b356b67b75c7bb..9802a5f5bd3bf7 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -666,112 +666,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "AlertsHistogram", - "description": "", - "args": [ - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - }, - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "stackByField", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "AlertsOverTimeData", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "AnomaliesHistogram", - "description": "", - "args": [ - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - }, - { - "name": "stackByField", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "AnomaliesOverTimeData", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "Authentications", "description": "Gets Authentication success and failures based on a timerange", @@ -833,59 +727,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "AuthenticationsHistogram", - "description": "", - "args": [ - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - }, - { - "name": "stackByField", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "AuthenticationsOverTimeData", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "Timeline", "description": "", @@ -1075,59 +916,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "EventsHistogram", - "description": "", - "args": [ - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - }, - { - "name": "stackByField", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "EventsOverTimeData", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "Hosts", "description": "Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified", @@ -1610,6 +1398,73 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "MatrixHistogram", + "description": "", + "args": [ + { + "name": "filterQuery", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "defaultIndex", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "defaultValue": null + }, + { + "name": "timerange", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "stackByField", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "histogramType", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "HistogramType", "ofType": null } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "MatrixHistogramOverTimeData", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "NetworkTopCountries", "description": "", @@ -2607,211 +2462,17 @@ }, { "name": "description", - "description": "Description of the field", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "format", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "TimerangeInput", - "description": "", - "fields": null, - "inputFields": [ - { - "name": "interval", - "description": "The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan.", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "to", - "description": "The end of the timerange", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "from", - "description": "The beginning of the timerange", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AlertsOverTimeData", - "description": "", - "fields": [ - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "matrixHistogramData", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "MatrixOverTimeHistogramData", - "ofType": null - } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "totalCount", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Inspect", - "description": "", - "fields": [ - { - "name": "dsl", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "response", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MatrixOverTimeHistogramData", - "description": "", - "fields": [ - { - "name": "x", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "y", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, + "description": "Description of the field", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "g", + "name": "format", "description": "", "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null } @@ -2822,57 +2483,43 @@ "possibleTypes": null }, { - "kind": "OBJECT", - "name": "AnomaliesOverTimeData", + "kind": "INPUT_OBJECT", + "name": "TimerangeInput", "description": "", - "fields": [ + "fields": null, + "inputFields": [ { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null + "name": "interval", + "description": "The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null }, { - "name": "matrixHistogramData", - "description": "", - "args": [], + "name": "to", + "description": "The end of the timerange", "type": { "kind": "NON_NULL", "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "MatrixOverTimeHistogramData", - "ofType": null - } - } - } + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } }, - "isDeprecated": false, - "deprecationReason": null + "defaultValue": null }, { - "name": "totalCount", - "description": "", - "args": [], + "name": "from", + "description": "The beginning of the timerange", "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } }, - "isDeprecated": false, - "deprecationReason": null + "defaultValue": null } ], - "inputFields": null, - "interfaces": [], + "interfaces": null, "enumValues": null, "possibleTypes": null }, @@ -3587,19 +3234,11 @@ }, { "kind": "OBJECT", - "name": "AuthenticationsOverTimeData", + "name": "Inspect", "description": "", "fields": [ { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "matrixHistogramData", + "name": "dsl", "description": "", "args": [], "type": { @@ -3611,11 +3250,7 @@ "ofType": { "kind": "NON_NULL", "name": null, - "ofType": { - "kind": "OBJECT", - "name": "MatrixOverTimeHistogramData", - "ofType": null - } + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } } }, @@ -3623,13 +3258,21 @@ "deprecationReason": null }, { - "name": "totalCount", + "name": "response", "description": "", "args": [], "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } }, "isDeprecated": false, "deprecationReason": null @@ -6639,61 +6282,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "EventsOverTimeData", - "description": "", - "fields": [ - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "matrixHistogramData", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "MatrixOverTimeHistogramData", - "ofType": null - } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "totalCount", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "INPUT_OBJECT", "name": "HostsSortField", @@ -7844,6 +7432,122 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "HistogramType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "authentications", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anomalies", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "events", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "alerts", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "dns", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MatrixHistogramOverTimeData", + "description": "", + "fields": [ + { + "name": "inspect", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "matrixHistogramData", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MatrixOverTimeHistogramData", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCount", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MatrixOverTimeHistogramData", + "description": "", + "fields": [ + { + "name": "x", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "y", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "g", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "ENUM", "name": "FlowTargetSourceDest", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index 0103713a8c8a2d..3528ee6e13a389 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -301,6 +301,14 @@ export enum FlowTarget { source = 'source', } +export enum HistogramType { + authentications = 'authentications', + anomalies = 'anomalies', + events = 'events', + alerts = 'alerts', + dns = 'dns', +} + export enum FlowTargetSourceDest { destination = 'destination', source = 'source', @@ -460,22 +468,14 @@ export interface Source { configuration: SourceConfiguration; /** The status of the source */ status: SourceStatus; - - AlertsHistogram: AlertsOverTimeData; - - AnomaliesHistogram: AnomaliesOverTimeData; /** Gets Authentication success and failures based on a timerange */ Authentications: AuthenticationsData; - AuthenticationsHistogram: AuthenticationsOverTimeData; - Timeline: TimelineData; TimelineDetails: TimelineDetailsData; LastEventTime: LastEventTimeData; - - EventsHistogram: EventsOverTimeData; /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ Hosts: HostsData; @@ -493,6 +493,8 @@ export interface Source { KpiHostDetails: KpiHostDetailsData; + MatrixHistogram: MatrixHistogramOverTimeData; + NetworkTopCountries: NetworkTopCountriesData; NetworkTopNFlow: NetworkTopNFlowData; @@ -566,36 +568,6 @@ export interface IndexField { format?: Maybe<string>; } -export interface AlertsOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - -export interface Inspect { - dsl: string[]; - - response: string[]; -} - -export interface MatrixOverTimeHistogramData { - x: number; - - y: number; - - g: string; -} - -export interface AnomaliesOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - export interface AuthenticationsData { edges: AuthenticationsEdges[]; @@ -730,12 +702,10 @@ export interface PageInfoPaginated { showMorePagesIndicator: boolean; } -export interface AuthenticationsOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; +export interface Inspect { + dsl: string[]; - totalCount: number; + response: string[]; } export interface TimelineData { @@ -1390,14 +1360,6 @@ export interface LastEventTimeData { inspect?: Maybe<Inspect>; } -export interface EventsOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - export interface HostsData { edges: HostsEdges[]; @@ -1598,6 +1560,22 @@ export interface KpiHostDetailsData { inspect?: Maybe<Inspect>; } +export interface MatrixHistogramOverTimeData { + inspect?: Maybe<Inspect>; + + matrixHistogramData: MatrixOverTimeHistogramData[]; + + totalCount: number; +} + +export interface MatrixOverTimeHistogramData { + x?: Maybe<number>; + + y?: Maybe<number>; + + g?: Maybe<string>; +} + export interface NetworkTopCountriesData { edges: NetworkTopCountriesEdges[]; @@ -2241,24 +2219,6 @@ export interface GetAllTimelineQueryArgs { onlyUserFavorite?: Maybe<boolean>; } -export interface AlertsHistogramSourceArgs { - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - timerange: TimerangeInput; - - stackByField?: Maybe<string>; -} -export interface AnomaliesHistogramSourceArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; -} export interface AuthenticationsSourceArgs { timerange: TimerangeInput; @@ -2268,15 +2228,6 @@ export interface AuthenticationsSourceArgs { defaultIndex: string[]; } -export interface AuthenticationsHistogramSourceArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; -} export interface TimelineSourceArgs { pagination: PaginationInput; @@ -2306,15 +2257,6 @@ export interface LastEventTimeSourceArgs { defaultIndex: string[]; } -export interface EventsHistogramSourceArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; -} export interface HostsSourceArgs { id?: Maybe<string>; @@ -2397,6 +2339,17 @@ export interface KpiHostDetailsSourceArgs { defaultIndex: string[]; } +export interface MatrixHistogramSourceArgs { + filterQuery?: Maybe<string>; + + defaultIndex: string[]; + + timerange: TimerangeInput; + + stackByField: string; + + histogramType: HistogramType; +} export interface NetworkTopCountriesSourceArgs { id?: Maybe<string>; @@ -3330,16 +3283,12 @@ export namespace GetKpiNetworkQuery { export namespace GetMatrixHistogramQuery { export type Variables = { - isAlertsHistogram: boolean; - isAnomaliesHistogram: boolean; - isAuthenticationsHistogram: boolean; - isDnsHistogram: boolean; defaultIndex: string[]; - isEventsHistogram: boolean; filterQuery?: Maybe<string>; + histogramType: HistogramType; inspect: boolean; sourceId: string; - stackByField?: Maybe<string>; + stackByField: string; timerange: TimerangeInput; }; @@ -3354,19 +3303,11 @@ export namespace GetMatrixHistogramQuery { id: string; - AlertsHistogram: AlertsHistogram; - - AnomaliesHistogram: AnomaliesHistogram; - - AuthenticationsHistogram: AuthenticationsHistogram; - - EventsHistogram: EventsHistogram; - - NetworkDnsHistogram: NetworkDnsHistogram; + MatrixHistogram: MatrixHistogram; }; - export type AlertsHistogram = { - __typename?: 'AlertsOverTimeData'; + export type MatrixHistogram = { + __typename?: 'MatrixHistogramOverTimeData'; matrixHistogramData: MatrixHistogramData[]; @@ -3378,11 +3319,11 @@ export namespace GetMatrixHistogramQuery { export type MatrixHistogramData = { __typename?: 'MatrixOverTimeHistogramData'; - x: number; + x: Maybe<number>; - y: number; + y: Maybe<number>; - g: string; + g: Maybe<string>; }; export type Inspect = { @@ -3392,118 +3333,6 @@ export namespace GetMatrixHistogramQuery { response: string[]; }; - - export type AnomaliesHistogram = { - __typename?: 'AnomaliesOverTimeData'; - - matrixHistogramData: _MatrixHistogramData[]; - - totalCount: number; - - inspect: Maybe<_Inspect>; - }; - - export type _MatrixHistogramData = { - __typename?: 'MatrixOverTimeHistogramData'; - - x: number; - - y: number; - - g: string; - }; - - export type _Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; - - export type AuthenticationsHistogram = { - __typename?: 'AuthenticationsOverTimeData'; - - matrixHistogramData: __MatrixHistogramData[]; - - totalCount: number; - - inspect: Maybe<__Inspect>; - }; - - export type __MatrixHistogramData = { - __typename?: 'MatrixOverTimeHistogramData'; - - x: number; - - y: number; - - g: string; - }; - - export type __Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; - - export type EventsHistogram = { - __typename?: 'EventsOverTimeData'; - - matrixHistogramData: ___MatrixHistogramData[]; - - totalCount: number; - - inspect: Maybe<___Inspect>; - }; - - export type ___MatrixHistogramData = { - __typename?: 'MatrixOverTimeHistogramData'; - - x: number; - - y: number; - - g: string; - }; - - export type ___Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; - - export type NetworkDnsHistogram = { - __typename?: 'NetworkDsOverTimeData'; - - matrixHistogramData: ____MatrixHistogramData[]; - - totalCount: number; - - inspect: Maybe<____Inspect>; - }; - - export type ____MatrixHistogramData = { - __typename?: 'MatrixOverTimeHistogramData'; - - x: number; - - y: number; - - g: string; - }; - - export type ____Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; } export namespace GetNetworkDnsQuery { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index 8a374617467736..8cfcac8fc862bd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -193,7 +193,6 @@ const DetectionEnginePageComponent: React.FC<DetectionEnginePageComponentProps> hideHeaderChildren={true} indexPattern={indexPattern} query={query} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!} setQuery={setQuery} to={to} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx index 2e2986fb632b19..06dffcdb220a94 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx @@ -29,6 +29,7 @@ import { useKibana } from '../../lib/kibana'; import { convertToBuildEsQuery } from '../../lib/keury'; import { inputsSelectors, State, hostsModel } from '../../store'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; + import { SpyRoute } from '../../utils/route/spy_routes'; import { esQuery } from '../../../../../../../src/plugins/data/public'; import { HostsEmptyPage } from './hosts_empty_page'; @@ -131,11 +132,11 @@ export const HostsComponent = React.memo<HostsComponentProps>( to={to} filterQuery={tabsFilterQuery} isInitializing={isInitializing} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setQuery={setQuery} from={from} type={hostsModel.HostsType.page} indexPattern={indexPattern} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} hostsPagePath={hostsPagePath} /> </WrapperPage> diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx index 9c13fc4ac386e5..0b83710a132935 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx @@ -52,9 +52,6 @@ const HostsTabs = memo<HostsTabsProps>( to: fromTo.to, }); }, - updateDateRange: (min: number, max: number) => { - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); - }, }; return ( diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx index a6a0344599842a..fb083b7a7da2f3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx @@ -14,11 +14,12 @@ import { hostsModel } from '../../../store/hosts'; import { MatrixHistogramOption, MatrixHistogramMappingTypes, + MatrixHisrogramConfigs, } from '../../../components/matrix_histogram/types'; -import { MatrixHistogramContainer } from '../../../containers/matrix_histogram'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; import { KpiHostsChartColors } from '../../../components/page/hosts/kpi_hosts/types'; -import { MatrixHistogramGqlQuery } from '../../../containers/matrix_histogram/index.gql_query'; import * as i18n from '../translations'; +import { HistogramType } from '../../../graphql/types'; const AuthenticationTableManage = manageQuery(AuthenticationTable); const ID = 'authenticationsOverTimeQuery'; @@ -28,6 +29,7 @@ const authStackByOptions: MatrixHistogramOption[] = [ value: 'event.type', }, ]; +const DEFAULT_STACK_BY = 'event.type'; enum AuthMatrixDataGroup { authSuccess = 'authentication_success', @@ -47,6 +49,16 @@ export const authMatrixDataMappingFields: MatrixHistogramMappingTypes = { }, }; +const histogramConfigs: MatrixHisrogramConfigs = { + defaultStackByOption: + authStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? authStackByOptions[0], + errorMessage: i18n.ERROR_FETCHING_AUTHENTICATIONS_DATA, + histogramType: HistogramType.authentications, + mapping: authMatrixDataMappingFields, + stackByOptions: authStackByOptions, + title: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, +}; + export const AuthenticationsQueryTabBody = ({ deleteQuery, endDate, @@ -55,7 +67,6 @@ export const AuthenticationsQueryTabBody = ({ setQuery, startDate, type, - updateDateRange = () => {}, }: HostsComponentsQueryProps) => { useEffect(() => { return () => { @@ -64,26 +75,18 @@ export const AuthenticationsQueryTabBody = ({ } }; }, [deleteQuery]); + return ( <> <MatrixHistogramContainer - isAuthenticationsHistogram={true} - dataKey="AuthenticationsHistogram" - defaultStackByOption={authStackByOptions[0]} endDate={endDate} - errorMessage={i18n.ERROR_FETCHING_AUTHENTICATIONS_DATA} filterQuery={filterQuery} id={ID} - mapping={authMatrixDataMappingFields} - query={MatrixHistogramGqlQuery} setQuery={setQuery} - skip={skip} sourceId="default" startDate={startDate} - stackByOptions={authStackByOptions} - title={i18n.NAVIGATION_AUTHENTICATIONS_TITLE} type={hostsModel.HostsType.page} - updateDateRange={updateDateRange} + {...histogramConfigs} /> <AuthenticationsQuery endDate={endDate} diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx index 0ea82ba53b3a22..cb2c19c642bc45 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx @@ -9,10 +9,13 @@ import { StatefulEventsViewer } from '../../../components/events_viewer'; import { HostsComponentsQueryProps } from './types'; import { hostsModel } from '../../../store/hosts'; import { eventsDefaultModel } from '../../../components/events_viewer/default_model'; -import { MatrixHistogramOption } from '../../../components/matrix_histogram/types'; -import { MatrixHistogramContainer } from '../../../containers/matrix_histogram'; -import { MatrixHistogramGqlQuery } from '../../../containers/matrix_histogram/index.gql_query'; +import { + MatrixHistogramOption, + MatrixHisrogramConfigs, +} from '../../../components/matrix_histogram/types'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; import * as i18n from '../translations'; +import { HistogramType } from '../../../graphql/types'; const HOSTS_PAGE_TIMELINE_ID = 'hosts-page'; const EVENTS_HISTOGRAM_ID = 'eventsOverTimeQuery'; @@ -32,15 +35,25 @@ export const eventsStackByOptions: MatrixHistogramOption[] = [ }, ]; +const DEFAULT_STACK_BY = 'event.action'; + +export const histogramConfigs: MatrixHisrogramConfigs = { + defaultStackByOption: + eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], + errorMessage: i18n.ERROR_FETCHING_EVENTS_DATA, + histogramType: HistogramType.events, + stackByOptions: eventsStackByOptions, + subtitle: undefined, + title: i18n.NAVIGATION_EVENTS_TITLE, +}; + export const EventsQueryTabBody = ({ deleteQuery, endDate, filterQuery, pageFilters, setQuery, - skip, startDate, - updateDateRange = () => {}, }: HostsComponentsQueryProps) => { useEffect(() => { return () => { @@ -49,25 +62,18 @@ export const EventsQueryTabBody = ({ } }; }, [deleteQuery]); + return ( <> <MatrixHistogramContainer - dataKey="EventsHistogram" - defaultStackByOption={eventsStackByOptions[0]} endDate={endDate} - isEventsHistogram={true} - errorMessage={i18n.ERROR_FETCHING_EVENTS_DATA} filterQuery={filterQuery} - query={MatrixHistogramGqlQuery} setQuery={setQuery} - skip={skip} sourceId="default" - stackByOptions={eventsStackByOptions} startDate={startDate} type={hostsModel.HostsType.page} - title={i18n.NAVIGATION_EVENTS_TITLE} - updateDateRange={updateDateRange} id={EVENTS_HISTOGRAM_ID} + {...histogramConfigs} /> <StatefulEventsViewer defaultModel={eventsDefaultModel} diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/types.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/types.ts index 5900937d2108e5..e6e2ebb9ac2fe2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/types.ts @@ -20,7 +20,7 @@ export interface HostsComponentReduxProps { filters: Filter[]; } -export interface HostsComponentDispatchProps { +interface HostsComponentDispatchProps { setAbsoluteRangeDatePicker: ActionCreator<{ id: InputsModelId; from: number; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx index b49849b285d8e7..fe456afcc7189b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback, useMemo } from 'react'; import { getOr } from 'lodash/fp'; import { NetworkDnsTable } from '../../../components/page/network/network_dns_table'; @@ -14,10 +14,13 @@ import { manageQuery } from '../../../components/page/manage_query'; import { NetworkComponentQueryProps } from './types'; import { networkModel } from '../../../store'; -import { MatrixHistogramOption } from '../../../components/matrix_histogram/types'; +import { + MatrixHistogramOption, + MatrixHisrogramConfigs, +} from '../../../components/matrix_histogram/types'; import * as i18n from '../translations'; -import { MatrixHistogramGqlQuery } from '../../../containers/matrix_histogram/index.gql_query'; -import { MatrixHistogramContainer } from '../../../containers/matrix_histogram'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { HistogramType } from '../../../graphql/types'; const NetworkDnsTableManage = manageQuery(NetworkDnsTable); @@ -28,6 +31,17 @@ const dnsStackByOptions: MatrixHistogramOption[] = [ }, ]; +const DEFAULT_STACK_BY = 'dns.question.registered_domain'; + +export const histogramConfigs: Omit<MatrixHisrogramConfigs, 'title'> = { + defaultStackByOption: + dnsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? dnsStackByOptions[0], + errorMessage: i18n.ERROR_FETCHING_DNS_DATA, + histogramType: HistogramType.dns, + stackByOptions: dnsStackByOptions, + subtitle: undefined, +}; + export const DnsQueryTabBody = ({ deleteQuery, endDate, @@ -36,7 +50,6 @@ export const DnsQueryTabBody = ({ startDate, setQuery, type, - updateDateRange = () => {}, }: NetworkComponentQueryProps) => { useEffect(() => { return () => { @@ -51,24 +64,26 @@ export const DnsQueryTabBody = ({ [] ); + const dnsHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + title: getTitle, + }), + [getTitle] + ); + return ( <> <MatrixHistogramContainer - dataKey={['NetworkDnsHistogram', 'matrixHistogramData']} - defaultStackByOption={dnsStackByOptions[0]} endDate={endDate} - errorMessage={i18n.ERROR_FETCHING_DNS_DATA} filterQuery={filterQuery} id={HISTOGRAM_ID} - isDnsHistogram={true} - query={MatrixHistogramGqlQuery} setQuery={setQuery} + showLegend={true} sourceId="default" startDate={startDate} - stackByOptions={dnsStackByOptions} - title={getTitle} type={networkModel.NetworkType.page} - updateDateRange={updateDateRange} + {...dnsHistogramConfigs} /> <NetworkDnsQuery endDate={endDate} diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx index acc5d02299f1fb..23a619db97ee4c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx @@ -45,12 +45,6 @@ export const NetworkRoutes = ({ }, [setAbsoluteRangeDatePicker] ); - const updateDateRange = useCallback( - (min: number, max: number) => { - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); - }, - [setAbsoluteRangeDatePicker] - ); const networkAnomaliesFilterQuery = { bool: { @@ -83,7 +77,6 @@ export const NetworkRoutes = ({ const tabProps = { ...commonProps, indexPattern, - updateDateRange, }; const anomaliesProps = { diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts index b6063a81f31f65..222a99992917d3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts @@ -13,7 +13,6 @@ import { ESTermQuery } from '../../../../common/typed_json'; import { GlobalTimeArgs } from '../../../containers/global_time'; import { SetAbsoluteRangeDatePicker } from '../types'; -import { UpdateDateRange } from '../../../components/charts/common'; import { NarrowDateRange } from '../../../components/ml/types'; interface QueryTabBodyProps extends Pick<GlobalTimeArgs, 'setQuery' | 'deleteQuery'> { @@ -22,7 +21,6 @@ interface QueryTabBodyProps extends Pick<GlobalTimeArgs, 'setQuery' | 'deleteQue startDate: number; endDate: number; filterQuery?: string | ESTermQuery; - updateDateRange?: UpdateDateRange; narrowDateRange?: NarrowDateRange; } diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx index 98ae3f30085a9b..f71d83558ae9d1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx @@ -6,21 +6,15 @@ import { EuiButton } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; +import { Position } from '@elastic/charts'; import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; -import { - ERROR_FETCHING_ALERTS_DATA, - SHOWING, - UNIT, -} from '../../../components/alerts_viewer/translations'; -import { alertsStackByOptions } from '../../../components/alerts_viewer'; +import { SHOWING, UNIT } from '../../../components/alerts_viewer/translations'; import { getDetectionEngineAlertUrl } from '../../../components/link_to/redirect_to_detection_engine'; -import { MatrixHistogramContainer } from '../../../containers/matrix_histogram'; -import { MatrixHistogramGqlQuery } from '../../../containers/matrix_histogram/index.gql_query'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; import { useKibana, useUiSetting$ } from '../../../lib/kibana'; import { convertToBuildEsQuery } from '../../../lib/keury'; -import { SetAbsoluteRangeDatePicker } from '../../network/types'; import { Filter, esQuery, @@ -31,6 +25,11 @@ import { inputsModel } from '../../../store'; import { HostsType } from '../../../store/hosts/model'; import * as i18n from '../translations'; +import { + alertsStackByOptions, + histogramConfigs, +} from '../../../components/alerts_viewer/histogram_configs'; +import { MatrixHisrogramConfigs } from '../../../components/matrix_histogram/types'; const ID = 'alertsByCategoryOverview'; @@ -45,7 +44,6 @@ interface Props { hideHeaderChildren?: boolean; indexPattern: IIndexPattern; query?: Query; - setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; setQuery: (params: { id: string; inspect: inputsModel.InspectQuery | null; @@ -62,7 +60,6 @@ const AlertsByCategoryComponent: React.FC<Props> = ({ hideHeaderChildren = false, indexPattern, query = DEFAULT_QUERY, - setAbsoluteRangeDatePicker, setQuery, to, }) => { @@ -77,32 +74,26 @@ const AlertsByCategoryComponent: React.FC<Props> = ({ const kibana = useKibana(); const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const updateDateRangeCallback = useCallback( - (min: number, max: number) => { - setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max }); - }, - [setAbsoluteRangeDatePicker] - ); const alertsCountViewAlertsButton = useMemo( () => <EuiButton href={getDetectionEngineAlertUrl()}>{i18n.VIEW_ALERTS}</EuiButton>, [] ); - const getSubtitle = useCallback( - (totalCount: number) => - `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, + const alertsByCategoryHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + defaultStackByOption: + alertsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0], + getSubtitle: (totalCount: number) => + `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, + legendPosition: Position.Right, + }), [] ); - const defaultStackByOption = - alertsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0]; - return ( <MatrixHistogramContainer - dataKey="AlertsHistogram" - defaultStackByOption={defaultStackByOption} endDate={to} - errorMessage={ERROR_FETCHING_ALERTS_DATA} filterQuery={convertToBuildEsQuery({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), indexPattern, @@ -111,17 +102,11 @@ const AlertsByCategoryComponent: React.FC<Props> = ({ })} headerChildren={hideHeaderChildren ? null : alertsCountViewAlertsButton} id={ID} - isAlertsHistogram={true} - legendPosition={'right'} - query={MatrixHistogramGqlQuery} setQuery={setQuery} sourceId="default" - stackByOptions={alertsStackByOptions} startDate={from} - title={i18n.ALERTS_GRAPH_TITLE} - subtitle={getSubtitle} type={HostsType.page} - updateDateRange={updateDateRangeCallback} + {...alertsByCategoryHistogramConfigs} /> ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx index 5b6ad69bcb15d0..315aac5fcae9ef 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx @@ -6,18 +6,14 @@ import { EuiButton } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; -import { - ERROR_FETCHING_EVENTS_DATA, - SHOWING, - UNIT, -} from '../../../components/events_viewer/translations'; +import { Position } from '@elastic/charts'; +import { SHOWING, UNIT } from '../../../components/events_viewer/translations'; import { convertToBuildEsQuery } from '../../../lib/keury'; -import { SetAbsoluteRangeDatePicker } from '../../network/types'; import { getTabsOnHostsUrl } from '../../../components/link_to/redirect_to_hosts'; -import { MatrixHistogramContainer } from '../../../containers/matrix_histogram'; -import { MatrixHistogramGqlQuery } from '../../../containers/matrix_histogram/index.gql_query'; +import { histogramConfigs } from '../../../pages/hosts/navigation/events_query_tab_body'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; import { eventsStackByOptions } from '../../hosts/navigation'; import { useKibana, useUiSetting$ } from '../../../lib/kibana'; import { @@ -31,6 +27,7 @@ import { HostsTableType, HostsType } from '../../../store/hosts/model'; import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; import * as i18n from '../translations'; +import { MatrixHisrogramConfigs } from '../../../components/matrix_histogram/types'; const NO_FILTERS: Filter[] = []; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; @@ -44,7 +41,6 @@ interface Props { from: number; indexPattern: IIndexPattern; query?: Query; - setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; setQuery: (params: { id: string; inspect: inputsModel.InspectQuery | null; @@ -60,7 +56,6 @@ const EventsByDatasetComponent: React.FC<Props> = ({ from, indexPattern, query = DEFAULT_QUERY, - setAbsoluteRangeDatePicker, setQuery, to, }) => { @@ -70,31 +65,16 @@ const EventsByDatasetComponent: React.FC<Props> = ({ deleteQuery({ id: ID }); } }; - }, []); + }, [deleteQuery]); const kibana = useKibana(); const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const updateDateRangeCallback = useCallback( - (min: number, max: number) => { - setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max }); - }, - [setAbsoluteRangeDatePicker] - ); const eventsCountViewEventsButton = useMemo( () => <EuiButton href={getTabsOnHostsUrl(HostsTableType.events)}>{i18n.VIEW_EVENTS}</EuiButton>, [] ); - const getSubtitle = useCallback( - (totalCount: number) => - `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, - [] - ); - - const defaultStackByOption = - eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0]; - const filterQuery = useMemo( () => convertToBuildEsQuery({ @@ -106,26 +86,29 @@ const EventsByDatasetComponent: React.FC<Props> = ({ [kibana, indexPattern, query, filters] ); + const eventsByDatasetHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + defaultStackByOption: + eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], + legendPosition: Position.Right, + subtitle: (totalCount: number) => + `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, + }), + [] + ); + return ( <MatrixHistogramContainer - dataKey="EventsHistogram" - defaultStackByOption={defaultStackByOption} endDate={to} - errorMessage={ERROR_FETCHING_EVENTS_DATA} filterQuery={filterQuery} headerChildren={eventsCountViewEventsButton} id={ID} - isEventsHistogram={true} - legendPosition={'right'} - query={MatrixHistogramGqlQuery} setQuery={setQuery} sourceId="default" - stackByOptions={eventsStackByOptions} startDate={from} - title={i18n.EVENTS} - subtitle={getSubtitle} type={HostsType.page} - updateDateRange={updateDateRangeCallback} + {...eventsByDatasetHistogramConfigs} /> ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx index 6f8446a6b1609f..8505b91fe1ff56 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx @@ -85,7 +85,6 @@ const OverviewComponent: React.FC<OverviewComponentReduxProps> = ({ from={from} indexPattern={indexPattern} query={query} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!} setQuery={setQuery} to={to} /> @@ -98,7 +97,6 @@ const OverviewComponent: React.FC<OverviewComponentReduxProps> = ({ from={from} indexPattern={indexPattern} query={query} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!} setQuery={setQuery} to={to} /> diff --git a/x-pack/legacy/plugins/siem/server/graphql/alerts/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/alerts/resolvers.ts deleted file mode 100644 index 5a3a50d5c6ec68..00000000000000 --- a/x-pack/legacy/plugins/siem/server/graphql/alerts/resolvers.ts +++ /dev/null @@ -1,39 +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 { Alerts } from '../../lib/alerts'; -import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; -import { createOptions } from '../../utils/build_query/create_options'; -import { QuerySourceResolver } from '../sources/resolvers'; -import { SourceResolvers } from '../types'; - -export interface AlertsResolversDeps { - alerts: Alerts; -} - -type QueryAlertsHistogramResolver = ChildResolverOf< - AppResolverOf<SourceResolvers.AlertsHistogramResolver>, - QuerySourceResolver ->; - -export const createAlertsResolvers = ( - libs: AlertsResolversDeps -): { - Source: { - AlertsHistogram: QueryAlertsHistogramResolver; - }; -} => ({ - Source: { - async AlertsHistogram(source, args, { req }, info) { - const options = { - ...createOptions(source, args, info), - defaultIndex: args.defaultIndex, - stackByField: args.stackByField, - }; - return libs.alerts.getAlertsHistogramData(req, options); - }, - }, -}); diff --git a/x-pack/legacy/plugins/siem/server/graphql/alerts/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/alerts/schema.gql.ts deleted file mode 100644 index ca91468b1e0f22..00000000000000 --- a/x-pack/legacy/plugins/siem/server/graphql/alerts/schema.gql.ts +++ /dev/null @@ -1,24 +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 gql from 'graphql-tag'; - -export const alertsSchema = gql` - type AlertsOverTimeData { - inspect: Inspect - matrixHistogramData: [MatrixOverTimeHistogramData!]! - totalCount: Float! - } - - extend type Source { - AlertsHistogram( - filterQuery: String - defaultIndex: [String!]! - timerange: TimerangeInput! - stackByField: String - ): AlertsOverTimeData! - } -`; diff --git a/x-pack/legacy/plugins/siem/server/graphql/authentications/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/authentications/resolvers.ts index ce1c86ac8926c4..b66ccd9a111b70 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/authentications/resolvers.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/authentications/resolvers.ts @@ -7,7 +7,7 @@ import { SourceResolvers } from '../../graphql/types'; import { Authentications } from '../../lib/authentications'; import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; -import { createOptionsPaginated, createOptions } from '../../utils/build_query/create_options'; +import { createOptionsPaginated } from '../../utils/build_query/create_options'; import { QuerySourceResolver } from '../sources/resolvers'; type QueryAuthenticationsResolver = ChildResolverOf< @@ -15,11 +15,6 @@ type QueryAuthenticationsResolver = ChildResolverOf< QuerySourceResolver >; -type QueryAuthenticationsOverTimeResolver = ChildResolverOf< - AppResolverOf<SourceResolvers.AuthenticationsHistogramResolver>, - QuerySourceResolver ->; - export interface AuthenticationsResolversDeps { authentications: Authentications; } @@ -29,7 +24,6 @@ export const createAuthenticationsResolvers = ( ): { Source: { Authentications: QueryAuthenticationsResolver; - AuthenticationsHistogram: QueryAuthenticationsOverTimeResolver; }; } => ({ Source: { @@ -37,13 +31,5 @@ export const createAuthenticationsResolvers = ( const options = createOptionsPaginated(source, args, info); return libs.authentications.getAuthentications(req, options); }, - async AuthenticationsHistogram(source, args, { req }, info) { - const options = { - ...createOptions(source, args, info), - defaultIndex: args.defaultIndex, - stackByField: args.stackByField, - }; - return libs.authentications.getAuthenticationsOverTime(req, options); - }, }, }); diff --git a/x-pack/legacy/plugins/siem/server/graphql/authentications/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/authentications/schema.gql.ts index 4acc72a5b0b6f1..20935ce9ed03fc 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/authentications/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/authentications/schema.gql.ts @@ -34,12 +34,6 @@ export const authenticationsSchema = gql` inspect: Inspect } - type AuthenticationsOverTimeData { - inspect: Inspect - matrixHistogramData: [MatrixOverTimeHistogramData!]! - totalCount: Float! - } - extend type Source { "Gets Authentication success and failures based on a timerange" Authentications( @@ -48,11 +42,5 @@ export const authenticationsSchema = gql` filterQuery: String defaultIndex: [String!]! ): AuthenticationsData! - AuthenticationsHistogram( - timerange: TimerangeInput! - filterQuery: String - defaultIndex: [String!]! - stackByField: String - ): AuthenticationsOverTimeData! } `; diff --git a/x-pack/legacy/plugins/siem/server/graphql/events/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/events/resolvers.ts index 335f4c3bf4da37..a9ef6bc682c845 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/events/resolvers.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/events/resolvers.ts @@ -31,12 +31,6 @@ type QueryLastEventTimeResolver = ChildResolverOf< export interface EventsResolversDeps { events: Events; } - -type QueryEventsOverTimeResolver = ChildResolverOf< - AppResolverOf<SourceResolvers.EventsHistogramResolver>, - QuerySourceResolver ->; - export const createEventsResolvers = ( libs: EventsResolversDeps ): { @@ -44,7 +38,6 @@ export const createEventsResolvers = ( Timeline: QueryTimelineResolver; TimelineDetails: QueryTimelineDetailsResolver; LastEventTime: QueryLastEventTimeResolver; - EventsHistogram: QueryEventsOverTimeResolver; }; } => ({ Source: { @@ -71,14 +64,6 @@ export const createEventsResolvers = ( }; return libs.events.getLastEventTimeData(req, options); }, - async EventsHistogram(source, args, { req }, info) { - const options = { - ...createOptions(source, args, info), - defaultIndex: args.defaultIndex, - stackByField: args.stackByField, - }; - return libs.events.getEventsOverTime(req, options); - }, }, }); diff --git a/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts index 9b321d10614fc2..3b71977bc0d478 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts @@ -68,18 +68,6 @@ export const eventsSchema = gql` network } - type MatrixOverTimeHistogramData { - x: Float! - y: Float! - g: String! - } - - type EventsOverTimeData { - inspect: Inspect - matrixHistogramData: [MatrixOverTimeHistogramData!]! - totalCount: Float! - } - extend type Source { Timeline( pagination: PaginationInput! @@ -100,11 +88,5 @@ export const eventsSchema = gql` details: LastTimeDetails! defaultIndex: [String!]! ): LastEventTimeData! - EventsHistogram( - timerange: TimerangeInput! - filterQuery: String - defaultIndex: [String!]! - stackByField: String - ): EventsOverTimeData! } `; diff --git a/x-pack/legacy/plugins/siem/server/graphql/index.ts b/x-pack/legacy/plugins/siem/server/graphql/index.ts index 60853e2ce7bed4..7e257357078939 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/index.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/index.ts @@ -7,7 +7,6 @@ import { rootSchema } from '../../common/graphql/root'; import { sharedSchema } from '../../common/graphql/shared'; -import { anomaliesSchema } from './anomalies'; import { authenticationsSchema } from './authentications'; import { ecsSchema } from './ecs'; import { eventsSchema } from './events'; @@ -30,10 +29,8 @@ import { timelineSchema } from './timeline'; import { tlsSchema } from './tls'; import { uncommonProcessesSchema } from './uncommon_processes'; import { whoAmISchema } from './who_am_i'; -import { alertsSchema } from './alerts'; +import { matrixHistogramSchema } from './matrix_histogram'; export const schemas = [ - alertsSchema, - anomaliesSchema, authenticationsSchema, ecsSchema, eventsSchema, @@ -46,6 +43,7 @@ export const schemas = [ ...ipDetailsSchemas, kpiNetworkSchema, kpiHostsSchema, + matrixHistogramSchema, networkSchema, noteSchema, overviewSchema, diff --git a/x-pack/legacy/plugins/siem/server/graphql/alerts/index.ts b/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/index.ts similarity index 67% rename from x-pack/legacy/plugins/siem/server/graphql/alerts/index.ts rename to x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/index.ts index f2beae525ed6b7..1460b6022bb133 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/alerts/index.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { createAlertsResolvers } from './resolvers'; -export { alertsSchema } from './schema.gql'; +export { createMatrixHistogramResolvers } from './resolvers'; +export { matrixHistogramSchema } from './schema.gql'; diff --git a/x-pack/legacy/plugins/siem/server/graphql/anomalies/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/resolvers.ts similarity index 55% rename from x-pack/legacy/plugins/siem/server/graphql/anomalies/resolvers.ts rename to x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/resolvers.ts index e7b7a640c58d23..35cebe4777dcf3 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/anomalies/resolvers.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/resolvers.ts @@ -4,36 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Anomalies } from '../../lib/anomalies'; +import { MatrixHistogram } from '../../lib/matrix_histogram'; import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; import { createOptions } from '../../utils/build_query/create_options'; import { QuerySourceResolver } from '../sources/resolvers'; import { SourceResolvers } from '../types'; -export interface AnomaliesResolversDeps { - anomalies: Anomalies; +export interface MatrixHistogramResolversDeps { + matrixHistogram: MatrixHistogram; } -type QueryAnomaliesOverTimeResolver = ChildResolverOf< - AppResolverOf<SourceResolvers.AnomaliesHistogramResolver>, +type QueryMatrixHistogramResolver = ChildResolverOf< + AppResolverOf<SourceResolvers.MatrixHistogramResolver>, QuerySourceResolver >; -export const createAnomaliesResolvers = ( - libs: AnomaliesResolversDeps +export const createMatrixHistogramResolvers = ( + libs: MatrixHistogramResolversDeps ): { Source: { - AnomaliesHistogram: QueryAnomaliesOverTimeResolver; + MatrixHistogram: QueryMatrixHistogramResolver; }; } => ({ Source: { - async AnomaliesHistogram(source, args, { req }, info) { + async MatrixHistogram(source, args, { req }, info) { const options = { ...createOptions(source, args, info), - defaultIndex: args.defaultIndex, stackByField: args.stackByField, + histogramType: args.histogramType, }; - return libs.anomalies.getAnomaliesOverTime(req, options); + return libs.matrixHistogram.getMatrixHistogramData(req, options); }, }, }); diff --git a/x-pack/legacy/plugins/siem/server/graphql/anomalies/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/schema.gql.ts similarity index 57% rename from x-pack/legacy/plugins/siem/server/graphql/anomalies/schema.gql.ts rename to x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/schema.gql.ts index a0b834f705696c..deda6dc6e5c1a7 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/anomalies/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/schema.gql.ts @@ -6,19 +6,34 @@ import gql from 'graphql-tag'; -export const anomaliesSchema = gql` - type AnomaliesOverTimeData { +export const matrixHistogramSchema = gql` + type MatrixOverTimeHistogramData { + x: Float + y: Float + g: String + } + + type MatrixHistogramOverTimeData { inspect: Inspect matrixHistogramData: [MatrixOverTimeHistogramData!]! totalCount: Float! } + enum HistogramType { + authentications + anomalies + events + alerts + dns + } + extend type Source { - AnomaliesHistogram( - timerange: TimerangeInput! + MatrixHistogram( filterQuery: String defaultIndex: [String!]! - stackByField: String - ): AnomaliesOverTimeData! + timerange: TimerangeInput! + stackByField: String! + histogramType: HistogramType! + ): MatrixHistogramOverTimeData! } `; diff --git a/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts index 06d6b8c516d8bd..db15babc42a722 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts @@ -7,7 +7,7 @@ import { SourceResolvers } from '../../graphql/types'; import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; import { Network } from '../../lib/network'; -import { createOptionsPaginated, createOptions } from '../../utils/build_query/create_options'; +import { createOptionsPaginated } from '../../utils/build_query/create_options'; import { QuerySourceResolver } from '../sources/resolvers'; type QueryNetworkTopCountriesResolver = ChildResolverOf< @@ -30,10 +30,6 @@ type QueryDnsResolver = ChildResolverOf< QuerySourceResolver >; -type QueryDnsHistogramResolver = ChildResolverOf< - AppResolverOf<SourceResolvers.NetworkDnsHistogramResolver>, - QuerySourceResolver ->; export interface NetworkResolversDeps { network: Network; } @@ -46,7 +42,6 @@ export const createNetworkResolvers = ( NetworkTopCountries: QueryNetworkTopCountriesResolver; NetworkTopNFlow: QueryNetworkTopNFlowResolver; NetworkDns: QueryDnsResolver; - NetworkDnsHistogram: QueryDnsHistogramResolver; }; } => ({ Source: { @@ -84,12 +79,5 @@ export const createNetworkResolvers = ( }; return libs.network.getNetworkDns(req, options); }, - async NetworkDnsHistogram(source, args, { req }, info) { - const options = { - ...createOptions(source, args, info), - stackByField: args.stackByField, - }; - return libs.network.getNetworkDnsHistogramData(req, options); - }, }, }); diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index c3fd6e9dde2865..f42da48f2c1dac 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -303,6 +303,14 @@ export enum FlowTarget { source = 'source', } +export enum HistogramType { + authentications = 'authentications', + anomalies = 'anomalies', + events = 'events', + alerts = 'alerts', + dns = 'dns', +} + export enum FlowTargetSourceDest { destination = 'destination', source = 'source', @@ -462,22 +470,14 @@ export interface Source { configuration: SourceConfiguration; /** The status of the source */ status: SourceStatus; - - AlertsHistogram: AlertsOverTimeData; - - AnomaliesHistogram: AnomaliesOverTimeData; /** Gets Authentication success and failures based on a timerange */ Authentications: AuthenticationsData; - AuthenticationsHistogram: AuthenticationsOverTimeData; - Timeline: TimelineData; TimelineDetails: TimelineDetailsData; LastEventTime: LastEventTimeData; - - EventsHistogram: EventsOverTimeData; /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ Hosts: HostsData; @@ -495,6 +495,8 @@ export interface Source { KpiHostDetails: KpiHostDetailsData; + MatrixHistogram: MatrixHistogramOverTimeData; + NetworkTopCountries: NetworkTopCountriesData; NetworkTopNFlow: NetworkTopNFlowData; @@ -568,36 +570,6 @@ export interface IndexField { format?: Maybe<string>; } -export interface AlertsOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - -export interface Inspect { - dsl: string[]; - - response: string[]; -} - -export interface MatrixOverTimeHistogramData { - x: number; - - y: number; - - g: string; -} - -export interface AnomaliesOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - export interface AuthenticationsData { edges: AuthenticationsEdges[]; @@ -732,12 +704,10 @@ export interface PageInfoPaginated { showMorePagesIndicator: boolean; } -export interface AuthenticationsOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; +export interface Inspect { + dsl: string[]; - totalCount: number; + response: string[]; } export interface TimelineData { @@ -1392,14 +1362,6 @@ export interface LastEventTimeData { inspect?: Maybe<Inspect>; } -export interface EventsOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - export interface HostsData { edges: HostsEdges[]; @@ -1600,6 +1562,22 @@ export interface KpiHostDetailsData { inspect?: Maybe<Inspect>; } +export interface MatrixHistogramOverTimeData { + inspect?: Maybe<Inspect>; + + matrixHistogramData: MatrixOverTimeHistogramData[]; + + totalCount: number; +} + +export interface MatrixOverTimeHistogramData { + x?: Maybe<number>; + + y?: Maybe<number>; + + g?: Maybe<string>; +} + export interface NetworkTopCountriesData { edges: NetworkTopCountriesEdges[]; @@ -2243,24 +2221,6 @@ export interface GetAllTimelineQueryArgs { onlyUserFavorite?: Maybe<boolean>; } -export interface AlertsHistogramSourceArgs { - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - timerange: TimerangeInput; - - stackByField?: Maybe<string>; -} -export interface AnomaliesHistogramSourceArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; -} export interface AuthenticationsSourceArgs { timerange: TimerangeInput; @@ -2270,15 +2230,6 @@ export interface AuthenticationsSourceArgs { defaultIndex: string[]; } -export interface AuthenticationsHistogramSourceArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; -} export interface TimelineSourceArgs { pagination: PaginationInput; @@ -2308,15 +2259,6 @@ export interface LastEventTimeSourceArgs { defaultIndex: string[]; } -export interface EventsHistogramSourceArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; -} export interface HostsSourceArgs { id?: Maybe<string>; @@ -2399,6 +2341,17 @@ export interface KpiHostDetailsSourceArgs { defaultIndex: string[]; } +export interface MatrixHistogramSourceArgs { + filterQuery?: Maybe<string>; + + defaultIndex: string[]; + + timerange: TimerangeInput; + + stackByField: string; + + histogramType: HistogramType; +} export interface NetworkTopCountriesSourceArgs { id?: Maybe<string>; @@ -2910,26 +2863,14 @@ export namespace SourceResolvers { configuration?: ConfigurationResolver<SourceConfiguration, TypeParent, TContext>; /** The status of the source */ status?: StatusResolver<SourceStatus, TypeParent, TContext>; - - AlertsHistogram?: AlertsHistogramResolver<AlertsOverTimeData, TypeParent, TContext>; - - AnomaliesHistogram?: AnomaliesHistogramResolver<AnomaliesOverTimeData, TypeParent, TContext>; /** Gets Authentication success and failures based on a timerange */ Authentications?: AuthenticationsResolver<AuthenticationsData, TypeParent, TContext>; - AuthenticationsHistogram?: AuthenticationsHistogramResolver< - AuthenticationsOverTimeData, - TypeParent, - TContext - >; - Timeline?: TimelineResolver<TimelineData, TypeParent, TContext>; TimelineDetails?: TimelineDetailsResolver<TimelineDetailsData, TypeParent, TContext>; LastEventTime?: LastEventTimeResolver<LastEventTimeData, TypeParent, TContext>; - - EventsHistogram?: EventsHistogramResolver<EventsOverTimeData, TypeParent, TContext>; /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ Hosts?: HostsResolver<HostsData, TypeParent, TContext>; @@ -2947,6 +2888,8 @@ export namespace SourceResolvers { KpiHostDetails?: KpiHostDetailsResolver<KpiHostDetailsData, TypeParent, TContext>; + MatrixHistogram?: MatrixHistogramResolver<MatrixHistogramOverTimeData, TypeParent, TContext>; + NetworkTopCountries?: NetworkTopCountriesResolver< NetworkTopCountriesData, TypeParent, @@ -2987,36 +2930,6 @@ export namespace SourceResolvers { Parent, TContext >; - export type AlertsHistogramResolver< - R = AlertsOverTimeData, - Parent = Source, - TContext = SiemContext - > = Resolver<R, Parent, TContext, AlertsHistogramArgs>; - export interface AlertsHistogramArgs { - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - timerange: TimerangeInput; - - stackByField?: Maybe<string>; - } - - export type AnomaliesHistogramResolver< - R = AnomaliesOverTimeData, - Parent = Source, - TContext = SiemContext - > = Resolver<R, Parent, TContext, AnomaliesHistogramArgs>; - export interface AnomaliesHistogramArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; - } - export type AuthenticationsResolver< R = AuthenticationsData, Parent = Source, @@ -3032,21 +2945,6 @@ export namespace SourceResolvers { defaultIndex: string[]; } - export type AuthenticationsHistogramResolver< - R = AuthenticationsOverTimeData, - Parent = Source, - TContext = SiemContext - > = Resolver<R, Parent, TContext, AuthenticationsHistogramArgs>; - export interface AuthenticationsHistogramArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; - } - export type TimelineResolver< R = TimelineData, Parent = Source, @@ -3094,21 +2992,6 @@ export namespace SourceResolvers { defaultIndex: string[]; } - export type EventsHistogramResolver< - R = EventsOverTimeData, - Parent = Source, - TContext = SiemContext - > = Resolver<R, Parent, TContext, EventsHistogramArgs>; - export interface EventsHistogramArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; - } - export type HostsResolver<R = HostsData, Parent = Source, TContext = SiemContext> = Resolver< R, Parent, @@ -3241,6 +3124,23 @@ export namespace SourceResolvers { defaultIndex: string[]; } + export type MatrixHistogramResolver< + R = MatrixHistogramOverTimeData, + Parent = Source, + TContext = SiemContext + > = Resolver<R, Parent, TContext, MatrixHistogramArgs>; + export interface MatrixHistogramArgs { + filterQuery?: Maybe<string>; + + defaultIndex: string[]; + + timerange: TimerangeInput; + + stackByField: string; + + histogramType: HistogramType; + } + export type NetworkTopCountriesResolver< R = NetworkTopCountriesData, Parent = Source, @@ -3579,111 +3479,6 @@ export namespace IndexFieldResolvers { > = Resolver<R, Parent, TContext>; } -export namespace AlertsOverTimeDataResolvers { - export interface Resolvers<TContext = SiemContext, TypeParent = AlertsOverTimeData> { - inspect?: InspectResolver<Maybe<Inspect>, TypeParent, TContext>; - - matrixHistogramData?: MatrixHistogramDataResolver< - MatrixOverTimeHistogramData[], - TypeParent, - TContext - >; - - totalCount?: TotalCountResolver<number, TypeParent, TContext>; - } - - export type InspectResolver< - R = Maybe<Inspect>, - Parent = AlertsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type MatrixHistogramDataResolver< - R = MatrixOverTimeHistogramData[], - Parent = AlertsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type TotalCountResolver< - R = number, - Parent = AlertsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; -} - -export namespace InspectResolvers { - export interface Resolvers<TContext = SiemContext, TypeParent = Inspect> { - dsl?: DslResolver<string[], TypeParent, TContext>; - - response?: ResponseResolver<string[], TypeParent, TContext>; - } - - export type DslResolver<R = string[], Parent = Inspect, TContext = SiemContext> = Resolver< - R, - Parent, - TContext - >; - export type ResponseResolver<R = string[], Parent = Inspect, TContext = SiemContext> = Resolver< - R, - Parent, - TContext - >; -} - -export namespace MatrixOverTimeHistogramDataResolvers { - export interface Resolvers<TContext = SiemContext, TypeParent = MatrixOverTimeHistogramData> { - x?: XResolver<number, TypeParent, TContext>; - - y?: YResolver<number, TypeParent, TContext>; - - g?: GResolver<string, TypeParent, TContext>; - } - - export type XResolver< - R = number, - Parent = MatrixOverTimeHistogramData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type YResolver< - R = number, - Parent = MatrixOverTimeHistogramData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type GResolver< - R = string, - Parent = MatrixOverTimeHistogramData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; -} - -export namespace AnomaliesOverTimeDataResolvers { - export interface Resolvers<TContext = SiemContext, TypeParent = AnomaliesOverTimeData> { - inspect?: InspectResolver<Maybe<Inspect>, TypeParent, TContext>; - - matrixHistogramData?: MatrixHistogramDataResolver< - MatrixOverTimeHistogramData[], - TypeParent, - TContext - >; - - totalCount?: TotalCountResolver<number, TypeParent, TContext>; - } - - export type InspectResolver< - R = Maybe<Inspect>, - Parent = AnomaliesOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type MatrixHistogramDataResolver< - R = MatrixOverTimeHistogramData[], - Parent = AnomaliesOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type TotalCountResolver< - R = number, - Parent = AnomaliesOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; -} - export namespace AuthenticationsDataResolvers { export interface Resolvers<TContext = SiemContext, TypeParent = AuthenticationsData> { edges?: EdgesResolver<AuthenticationsEdges[], TypeParent, TContext>; @@ -4129,34 +3924,23 @@ export namespace PageInfoPaginatedResolvers { > = Resolver<R, Parent, TContext>; } -export namespace AuthenticationsOverTimeDataResolvers { - export interface Resolvers<TContext = SiemContext, TypeParent = AuthenticationsOverTimeData> { - inspect?: InspectResolver<Maybe<Inspect>, TypeParent, TContext>; - - matrixHistogramData?: MatrixHistogramDataResolver< - MatrixOverTimeHistogramData[], - TypeParent, - TContext - >; +export namespace InspectResolvers { + export interface Resolvers<TContext = SiemContext, TypeParent = Inspect> { + dsl?: DslResolver<string[], TypeParent, TContext>; - totalCount?: TotalCountResolver<number, TypeParent, TContext>; + response?: ResponseResolver<string[], TypeParent, TContext>; } - export type InspectResolver< - R = Maybe<Inspect>, - Parent = AuthenticationsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type MatrixHistogramDataResolver< - R = MatrixOverTimeHistogramData[], - Parent = AuthenticationsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type TotalCountResolver< - R = number, - Parent = AuthenticationsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; + export type DslResolver<R = string[], Parent = Inspect, TContext = SiemContext> = Resolver< + R, + Parent, + TContext + >; + export type ResponseResolver<R = string[], Parent = Inspect, TContext = SiemContext> = Resolver< + R, + Parent, + TContext + >; } export namespace TimelineDataResolvers { @@ -6343,36 +6127,6 @@ export namespace LastEventTimeDataResolvers { > = Resolver<R, Parent, TContext>; } -export namespace EventsOverTimeDataResolvers { - export interface Resolvers<TContext = SiemContext, TypeParent = EventsOverTimeData> { - inspect?: InspectResolver<Maybe<Inspect>, TypeParent, TContext>; - - matrixHistogramData?: MatrixHistogramDataResolver< - MatrixOverTimeHistogramData[], - TypeParent, - TContext - >; - - totalCount?: TotalCountResolver<number, TypeParent, TContext>; - } - - export type InspectResolver< - R = Maybe<Inspect>, - Parent = EventsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type MatrixHistogramDataResolver< - R = MatrixOverTimeHistogramData[], - Parent = EventsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type TotalCountResolver< - R = number, - Parent = EventsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; -} - export namespace HostsDataResolvers { export interface Resolvers<TContext = SiemContext, TypeParent = HostsData> { edges?: EdgesResolver<HostsEdges[], TypeParent, TContext>; @@ -7077,6 +6831,62 @@ export namespace KpiHostDetailsDataResolvers { > = Resolver<R, Parent, TContext>; } +export namespace MatrixHistogramOverTimeDataResolvers { + export interface Resolvers<TContext = SiemContext, TypeParent = MatrixHistogramOverTimeData> { + inspect?: InspectResolver<Maybe<Inspect>, TypeParent, TContext>; + + matrixHistogramData?: MatrixHistogramDataResolver< + MatrixOverTimeHistogramData[], + TypeParent, + TContext + >; + + totalCount?: TotalCountResolver<number, TypeParent, TContext>; + } + + export type InspectResolver< + R = Maybe<Inspect>, + Parent = MatrixHistogramOverTimeData, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; + export type MatrixHistogramDataResolver< + R = MatrixOverTimeHistogramData[], + Parent = MatrixHistogramOverTimeData, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; + export type TotalCountResolver< + R = number, + Parent = MatrixHistogramOverTimeData, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; +} + +export namespace MatrixOverTimeHistogramDataResolvers { + export interface Resolvers<TContext = SiemContext, TypeParent = MatrixOverTimeHistogramData> { + x?: XResolver<Maybe<number>, TypeParent, TContext>; + + y?: YResolver<Maybe<number>, TypeParent, TContext>; + + g?: GResolver<Maybe<string>, TypeParent, TContext>; + } + + export type XResolver< + R = Maybe<number>, + Parent = MatrixOverTimeHistogramData, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; + export type YResolver< + R = Maybe<number>, + Parent = MatrixOverTimeHistogramData, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; + export type GResolver< + R = Maybe<string>, + Parent = MatrixOverTimeHistogramData, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; +} + export namespace NetworkTopCountriesDataResolvers { export interface Resolvers<TContext = SiemContext, TypeParent = NetworkTopCountriesData> { edges?: EdgesResolver<NetworkTopCountriesEdges[], TypeParent, TContext>; @@ -9224,10 +9034,6 @@ export type IResolvers<TContext = SiemContext> = { SourceFields?: SourceFieldsResolvers.Resolvers<TContext>; SourceStatus?: SourceStatusResolvers.Resolvers<TContext>; IndexField?: IndexFieldResolvers.Resolvers<TContext>; - AlertsOverTimeData?: AlertsOverTimeDataResolvers.Resolvers<TContext>; - Inspect?: InspectResolvers.Resolvers<TContext>; - MatrixOverTimeHistogramData?: MatrixOverTimeHistogramDataResolvers.Resolvers<TContext>; - AnomaliesOverTimeData?: AnomaliesOverTimeDataResolvers.Resolvers<TContext>; AuthenticationsData?: AuthenticationsDataResolvers.Resolvers<TContext>; AuthenticationsEdges?: AuthenticationsEdgesResolvers.Resolvers<TContext>; AuthenticationItem?: AuthenticationItemResolvers.Resolvers<TContext>; @@ -9240,7 +9046,7 @@ export type IResolvers<TContext = SiemContext> = { OsEcsFields?: OsEcsFieldsResolvers.Resolvers<TContext>; CursorType?: CursorTypeResolvers.Resolvers<TContext>; PageInfoPaginated?: PageInfoPaginatedResolvers.Resolvers<TContext>; - AuthenticationsOverTimeData?: AuthenticationsOverTimeDataResolvers.Resolvers<TContext>; + Inspect?: InspectResolvers.Resolvers<TContext>; TimelineData?: TimelineDataResolvers.Resolvers<TContext>; TimelineEdges?: TimelineEdgesResolvers.Resolvers<TContext>; TimelineItem?: TimelineItemResolvers.Resolvers<TContext>; @@ -9294,7 +9100,6 @@ export type IResolvers<TContext = SiemContext> = { TimelineDetailsData?: TimelineDetailsDataResolvers.Resolvers<TContext>; DetailItem?: DetailItemResolvers.Resolvers<TContext>; LastEventTimeData?: LastEventTimeDataResolvers.Resolvers<TContext>; - EventsOverTimeData?: EventsOverTimeDataResolvers.Resolvers<TContext>; HostsData?: HostsDataResolvers.Resolvers<TContext>; HostsEdges?: HostsEdgesResolvers.Resolvers<TContext>; HostItem?: HostItemResolvers.Resolvers<TContext>; @@ -9315,6 +9120,8 @@ export type IResolvers<TContext = SiemContext> = { KpiHostsData?: KpiHostsDataResolvers.Resolvers<TContext>; KpiHostHistogramData?: KpiHostHistogramDataResolvers.Resolvers<TContext>; KpiHostDetailsData?: KpiHostDetailsDataResolvers.Resolvers<TContext>; + MatrixHistogramOverTimeData?: MatrixHistogramOverTimeDataResolvers.Resolvers<TContext>; + MatrixOverTimeHistogramData?: MatrixOverTimeHistogramDataResolvers.Resolvers<TContext>; NetworkTopCountriesData?: NetworkTopCountriesDataResolvers.Resolvers<TContext>; NetworkTopCountriesEdges?: NetworkTopCountriesEdgesResolvers.Resolvers<TContext>; NetworkTopCountriesItem?: NetworkTopCountriesItemResolvers.Resolvers<TContext>; diff --git a/x-pack/legacy/plugins/siem/server/init_server.ts b/x-pack/legacy/plugins/siem/server/init_server.ts index 1f4f1b176497fd..6158a33c25cfa2 100644 --- a/x-pack/legacy/plugins/siem/server/init_server.ts +++ b/x-pack/legacy/plugins/siem/server/init_server.ts @@ -6,7 +6,6 @@ import { IResolvers, makeExecutableSchema } from 'graphql-tools'; import { schemas } from './graphql'; -import { createAnomaliesResolvers } from './graphql/anomalies'; import { createAuthenticationsResolvers } from './graphql/authentications'; import { createScalarToStringArrayValueResolvers } from './graphql/ecs'; import { createEsValueResolvers, createEventsResolvers } from './graphql/events'; @@ -30,19 +29,18 @@ import { createUncommonProcessesResolvers } from './graphql/uncommon_processes'; import { createWhoAmIResolvers } from './graphql/who_am_i'; import { AppBackendLibs } from './lib/types'; import { createTlsResolvers } from './graphql/tls'; -import { createAlertsResolvers } from './graphql/alerts'; +import { createMatrixHistogramResolvers } from './graphql/matrix_histogram'; export const initServer = (libs: AppBackendLibs) => { const schema = makeExecutableSchema({ resolvers: [ - createAlertsResolvers(libs) as IResolvers, - createAnomaliesResolvers(libs) as IResolvers, createAuthenticationsResolvers(libs) as IResolvers, createEsValueResolvers() as IResolvers, createEventsResolvers(libs) as IResolvers, createHostsResolvers(libs) as IResolvers, createIpDetailsResolvers(libs) as IResolvers, createKpiNetworkResolvers(libs) as IResolvers, + createMatrixHistogramResolvers(libs) as IResolvers, createNoteResolvers(libs) as IResolvers, createPinnedEventResolvers(libs) as IResolvers, createSourcesResolvers(libs) as IResolvers, diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/alerts/elasticsearch_adapter.ts deleted file mode 100644 index cedd7815968120..00000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/alerts/elasticsearch_adapter.ts +++ /dev/null @@ -1,63 +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 { get, getOr } from 'lodash/fp'; - -import { AlertsOverTimeData, MatrixOverTimeHistogramData } from '../../graphql/types'; - -import { inspectStringifyObject } from '../../utils/build_query'; - -import { FrameworkAdapter, FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; -import { buildAlertsHistogramQuery } from './query.dsl'; - -import { AlertsAdapter, AlertsGroupData, AlertsBucket } from './types'; -import { TermAggregation } from '../types'; -import { EventHit } from '../events/types'; - -export class ElasticsearchAlertsAdapter implements AlertsAdapter { - constructor(private readonly framework: FrameworkAdapter) {} - - public async getAlertsHistogramData( - request: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AlertsOverTimeData> { - const dsl = buildAlertsHistogramQuery(options); - const response = await this.framework.callWithRequest<EventHit, TermAggregation>( - request, - 'search', - dsl - ); - const totalCount = getOr(0, 'hits.total.value', response); - const matrixHistogramData = getOr([], 'aggregations.alertsByModuleGroup.buckets', response); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - return { - inspect, - matrixHistogramData: getAlertsOverTimeByModule(matrixHistogramData), - totalCount, - }; - } -} - -const getAlertsOverTimeByModule = (data: AlertsGroupData[]): MatrixOverTimeHistogramData[] => { - let result: MatrixOverTimeHistogramData[] = []; - data.forEach(({ key: group, alerts }) => { - const alertsData: AlertsBucket[] = get('buckets', alerts); - - result = [ - ...result, - ...alertsData.map(({ key, doc_count }: AlertsBucket) => ({ - x: key, - y: doc_count, - g: group, - })), - ]; - }); - - return result; -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/index.ts b/x-pack/legacy/plugins/siem/server/lib/alerts/index.ts deleted file mode 100644 index 9cfb1841edfefe..00000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/alerts/index.ts +++ /dev/null @@ -1,21 +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 { FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; -export * from './elasticsearch_adapter'; -import { AlertsAdapter } from './types'; -import { AlertsOverTimeData } from '../../graphql/types'; - -export class Alerts { - constructor(private readonly adapter: AlertsAdapter) {} - - public async getAlertsHistogramData( - req: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AlertsOverTimeData> { - return this.adapter.getAlertsHistogramData(req, options); - } -} diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/alerts/types.ts deleted file mode 100644 index 67da38e8052d29..00000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/alerts/types.ts +++ /dev/null @@ -1,27 +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 { AlertsOverTimeData } from '../../graphql/types'; -import { FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; - -export interface AlertsBucket { - key: number; - doc_count: number; -} - -export interface AlertsGroupData { - key: string; - doc_count: number; - alerts: { - buckets: AlertsBucket[]; - }; -} -export interface AlertsAdapter { - getAlertsHistogramData( - request: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AlertsOverTimeData>; -} diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/anomalies/elasticsearch_adapter.ts deleted file mode 100644 index 0955bc69c7c939..00000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/anomalies/elasticsearch_adapter.ts +++ /dev/null @@ -1,64 +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 { getOr } from 'lodash/fp'; - -import { AnomaliesOverTimeData } from '../../graphql/types'; -import { inspectStringifyObject } from '../../utils/build_query'; -import { FrameworkAdapter, FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; -import { TermAggregation } from '../types'; - -import { AnomalyHit, AnomaliesAdapter, AnomaliesActionGroupData } from './types'; -import { buildAnomaliesOverTimeQuery } from './query.anomalies_over_time.dsl'; -import { MatrixOverTimeHistogramData } from '../../../public/graphql/types'; - -export class ElasticsearchAnomaliesAdapter implements AnomaliesAdapter { - constructor(private readonly framework: FrameworkAdapter) {} - - public async getAnomaliesOverTime( - request: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AnomaliesOverTimeData> { - const dsl = buildAnomaliesOverTimeQuery(options); - - const response = await this.framework.callWithRequest<AnomalyHit, TermAggregation>( - request, - 'search', - dsl - ); - - const totalCount = getOr(0, 'hits.total.value', response); - const anomaliesOverTimeBucket = getOr([], 'aggregations.anomalyActionGroup.buckets', response); - - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - return { - inspect, - matrixHistogramData: getAnomaliesOverTimeByJobId(anomaliesOverTimeBucket), - totalCount, - }; - } -} - -const getAnomaliesOverTimeByJobId = ( - data: AnomaliesActionGroupData[] -): MatrixOverTimeHistogramData[] => { - let result: MatrixOverTimeHistogramData[] = []; - data.forEach(({ key: group, anomalies }) => { - const anomaliesData = getOr([], 'buckets', anomalies).map( - ({ key, doc_count }: { key: number; doc_count: number }) => ({ - x: key, - y: doc_count, - g: group, - }) - ); - result = [...result, ...anomaliesData]; - }); - - return result; -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/types.ts b/x-pack/legacy/plugins/siem/server/lib/anomalies/types.ts deleted file mode 100644 index 9fde81da63ec79..00000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/anomalies/types.ts +++ /dev/null @@ -1,42 +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 { AnomaliesOverTimeData } from '../../graphql/types'; -import { FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; -import { SearchHit } from '../types'; - -export interface AnomaliesAdapter { - getAnomaliesOverTime( - req: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AnomaliesOverTimeData>; -} - -export interface AnomalySource { - [field: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any -} - -export interface AnomalyHit extends SearchHit { - sort: string[]; - _source: AnomalySource; - aggregations: { - [agg: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any - }; -} - -interface AnomaliesOverTimeHistogramData { - key_as_string: string; - key: number; - doc_count: number; -} - -export interface AnomaliesActionGroupData { - key: number; - anomalies: { - bucket: AnomaliesOverTimeHistogramData[]; - }; - doc_count: number; -} diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts index 85008adcd985f8..79f13ce4461e5a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts @@ -6,50 +6,20 @@ import { getOr } from 'lodash/fp'; -import { - AuthenticationsData, - AuthenticationsEdges, - AuthenticationsOverTimeData, - MatrixOverTimeHistogramData, -} from '../../graphql/types'; +import { AuthenticationsData, AuthenticationsEdges } from '../../graphql/types'; import { mergeFieldsWithHit, inspectStringifyObject } from '../../utils/build_query'; -import { - FrameworkAdapter, - FrameworkRequest, - RequestOptionsPaginated, - MatrixHistogramRequestOptions, -} from '../framework'; +import { FrameworkAdapter, FrameworkRequest, RequestOptionsPaginated } from '../framework'; import { TermAggregation } from '../types'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; import { auditdFieldsMap, buildQuery } from './query.dsl'; -import { buildAuthenticationsOverTimeQuery } from './query.authentications_over_time.dsl'; import { AuthenticationBucket, AuthenticationData, AuthenticationHit, AuthenticationsAdapter, - AuthenticationsActionGroupData, } from './types'; -const getAuthenticationsOverTimeByAuthenticationResult = ( - data: AuthenticationsActionGroupData[] -): MatrixOverTimeHistogramData[] => { - let result: MatrixOverTimeHistogramData[] = []; - data.forEach(({ key: group, events }) => { - const eventsData = getOr([], 'buckets', events).map( - ({ key, doc_count }: { key: number; doc_count: number }) => ({ - x: key, - y: doc_count, - g: group, - }) - ); - result = [...result, ...eventsData]; - }); - - return result; -}; - export class ElasticsearchAuthenticationAdapter implements AuthenticationsAdapter { constructor(private readonly framework: FrameworkAdapter) {} @@ -109,35 +79,6 @@ export class ElasticsearchAuthenticationAdapter implements AuthenticationsAdapte }, }; } - - public async getAuthenticationsOverTime( - request: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AuthenticationsOverTimeData> { - const dsl = buildAuthenticationsOverTimeQuery(options); - const response = await this.framework.callWithRequest<AuthenticationHit, TermAggregation>( - request, - 'search', - dsl - ); - const totalCount = getOr(0, 'hits.total.value', response); - const authenticationsOverTimeBucket = getOr( - [], - 'aggregations.eventActionGroup.buckets', - response - ); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - return { - inspect, - matrixHistogramData: getAuthenticationsOverTimeByAuthenticationResult( - authenticationsOverTimeBucket - ), - totalCount, - }; - } } export const formatAuthenticationData = ( diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/index.ts b/x-pack/legacy/plugins/siem/server/lib/authentications/index.ts index bd5712c105f31d..c1b93818943dbe 100644 --- a/x-pack/legacy/plugins/siem/server/lib/authentications/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/authentications/index.ts @@ -5,14 +5,9 @@ */ import { AuthenticationsData } from '../../graphql/types'; -import { - FrameworkRequest, - RequestOptionsPaginated, - MatrixHistogramRequestOptions, -} from '../framework'; +import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; import { AuthenticationsAdapter } from './types'; -import { AuthenticationsOverTimeData } from '../../../public/graphql/types'; export class Authentications { constructor(private readonly adapter: AuthenticationsAdapter) {} @@ -23,11 +18,4 @@ export class Authentications { ): Promise<AuthenticationsData> { return this.adapter.getAuthentications(req, options); } - - public async getAuthenticationsOverTime( - req: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AuthenticationsOverTimeData> { - return this.adapter.getAuthenticationsOverTime(req, options); - } } diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/types.ts b/x-pack/legacy/plugins/siem/server/lib/authentications/types.ts index e1ec871ff4b589..2d2c7ba547c094 100644 --- a/x-pack/legacy/plugins/siem/server/lib/authentications/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/authentications/types.ts @@ -4,16 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - AuthenticationsData, - AuthenticationsOverTimeData, - LastSourceHost, -} from '../../graphql/types'; -import { - FrameworkRequest, - RequestOptionsPaginated, - MatrixHistogramRequestOptions, -} from '../framework'; +import { AuthenticationsData, LastSourceHost } from '../../graphql/types'; +import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; import { Hit, SearchHit, TotalHit } from '../types'; export interface AuthenticationsAdapter { @@ -21,10 +13,6 @@ export interface AuthenticationsAdapter { req: FrameworkRequest, options: RequestOptionsPaginated ): Promise<AuthenticationsData>; - getAuthenticationsOverTime( - req: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AuthenticationsOverTimeData>; } type StringOrNumber = string | number; @@ -72,17 +60,3 @@ export interface AuthenticationData extends SearchHit { }; }; } - -interface AuthenticationsOverTimeHistogramData { - key_as_string: string; - key: number; - doc_count: number; -} - -export interface AuthenticationsActionGroupData { - key: number; - events: { - bucket: AuthenticationsOverTimeHistogramData[]; - }; - doc_count: number; -} diff --git a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts index 0ab6f1a8df779d..9c46f3320e37ec 100644 --- a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts @@ -6,8 +6,6 @@ import { CoreSetup, SetupPlugins } from '../../plugin'; -import { Anomalies } from '../anomalies'; -import { ElasticsearchAnomaliesAdapter } from '../anomalies/elasticsearch_adapter'; import { Authentications } from '../authentications'; import { ElasticsearchAuthenticationAdapter } from '../authentications/elasticsearch_adapter'; import { ElasticsearchEventsAdapter, Events } from '../events'; @@ -32,7 +30,7 @@ import { ElasticsearchUncommonProcessesAdapter, UncommonProcesses } from '../unc import { Note } from '../note/saved_object'; import { PinnedEvent } from '../pinned_event/saved_object'; import { Timeline } from '../timeline/saved_object'; -import { Alerts, ElasticsearchAlertsAdapter } from '../alerts'; +import { ElasticsearchMatrixHistogramAdapter, MatrixHistogram } from '../matrix_histogram'; export function compose( core: CoreSetup, @@ -48,8 +46,6 @@ export function compose( const pinnedEvent = new PinnedEvent(); const domainLibs: AppDomainLibs = { - alerts: new Alerts(new ElasticsearchAlertsAdapter(framework)), - anomalies: new Anomalies(new ElasticsearchAnomaliesAdapter(framework)), authentications: new Authentications(new ElasticsearchAuthenticationAdapter(framework)), events: new Events(new ElasticsearchEventsAdapter(framework)), fields: new IndexFields(new ElasticsearchIndexFieldAdapter(framework)), @@ -58,6 +54,7 @@ export function compose( tls: new TLS(new ElasticsearchTlsAdapter(framework)), kpiHosts: new KpiHosts(new ElasticsearchKpiHostsAdapter(framework)), kpiNetwork: new KpiNetwork(new ElasticsearchKpiNetworkAdapter(framework)), + matrixHistogram: new MatrixHistogram(new ElasticsearchMatrixHistogramAdapter(framework)), network: new Network(new ElasticsearchNetworkAdapter(framework)), overview: new Overview(new ElasticsearchOverviewAdapter(framework)), uncommonProcesses: new UncommonProcesses(new ElasticsearchUncommonProcessesAdapter(framework)), diff --git a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts index 38b95cc5772f2e..af6f8314b362a1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts @@ -25,13 +25,12 @@ import { TimelineData, TimelineDetailsData, TimelineEdges, - EventsOverTimeData, } from '../../graphql/types'; import { baseCategoryFields } from '../../utils/beat_schema/8.0.0'; import { reduceFields } from '../../utils/build_query/reduce_fields'; import { mergeFieldsWithHit, inspectStringifyObject } from '../../utils/build_query'; import { eventFieldsMap } from '../ecs_fields'; -import { FrameworkAdapter, FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; +import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { TermAggregation } from '../types'; import { buildDetailsQuery, buildTimelineQuery } from './query.dsl'; @@ -43,10 +42,7 @@ import { LastEventTimeRequestOptions, RequestDetailsOptions, TimelineRequestOptions, - EventsActionGroupData, } from './types'; -import { buildEventsOverTimeQuery } from './query.events_over_time.dsl'; -import { MatrixOverTimeHistogramData } from '../../../public/graphql/types'; export class ElasticsearchEventsAdapter implements EventsAdapter { constructor(private readonly framework: FrameworkAdapter) {} @@ -129,65 +125,8 @@ export class ElasticsearchEventsAdapter implements EventsAdapter { lastSeen: getOr(null, 'aggregations.last_seen_event.value_as_string', response), }; } - - public async getEventsOverTime( - request: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<EventsOverTimeData> { - const dsl = buildEventsOverTimeQuery(options); - const response = await this.framework.callWithRequest<EventHit, TermAggregation>( - request, - 'search', - dsl - ); - const totalCount = getOr(0, 'hits.total.value', response); - const eventsOverTimeBucket = getOr([], 'aggregations.eventActionGroup.buckets', response); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - return { - inspect, - matrixHistogramData: getEventsOverTimeByActionName(eventsOverTimeBucket), - totalCount, - }; - } } -/** - * Not in use at the moment, - * reserved this parser for next feature of switchign between total events and grouped events - */ -export const getTotalEventsOverTime = ( - data: EventsActionGroupData[] -): MatrixOverTimeHistogramData[] => { - return data && data.length > 0 - ? data.map<MatrixOverTimeHistogramData>(({ key, doc_count }) => ({ - x: key, - y: doc_count, - g: 'total events', - })) - : []; -}; - -const getEventsOverTimeByActionName = ( - data: EventsActionGroupData[] -): MatrixOverTimeHistogramData[] => { - let result: MatrixOverTimeHistogramData[] = []; - data.forEach(({ key: group, events }) => { - const eventsData = getOr([], 'buckets', events).map( - ({ key, doc_count }: { key: number; doc_count: number }) => ({ - x: key, - y: doc_count, - g: group, - }) - ); - result = [...result, ...eventsData]; - }); - - return result; -}; - export const formatEventsData = ( fields: readonly string[], hit: EventHit, diff --git a/x-pack/legacy/plugins/siem/server/lib/events/index.ts b/x-pack/legacy/plugins/siem/server/lib/events/index.ts index 9e2457904f8c0d..9c1f87aa3d8bf9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/index.ts @@ -5,7 +5,7 @@ */ import { LastEventTimeData, TimelineData, TimelineDetailsData } from '../../graphql/types'; -import { FrameworkRequest, RequestBasicOptions } from '../framework'; +import { FrameworkRequest } from '../framework'; export * from './elasticsearch_adapter'; import { EventsAdapter, @@ -13,7 +13,6 @@ import { LastEventTimeRequestOptions, RequestDetailsOptions, } from './types'; -import { EventsOverTimeData } from '../../../public/graphql/types'; export class Events { constructor(private readonly adapter: EventsAdapter) {} @@ -38,11 +37,4 @@ export class Events { ): Promise<LastEventTimeData> { return this.adapter.getLastEventTimeData(req, options); } - - public async getEventsOverTime( - req: FrameworkRequest, - options: RequestBasicOptions - ): Promise<EventsOverTimeData> { - return this.adapter.getEventsOverTime(req, options); - } } diff --git a/x-pack/legacy/plugins/siem/server/lib/events/types.ts b/x-pack/legacy/plugins/siem/server/lib/events/types.ts index 2da0ff13638e1d..3a4a8705f73873 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/types.ts @@ -11,14 +11,8 @@ import { SourceConfiguration, TimelineData, TimelineDetailsData, - EventsOverTimeData, } from '../../graphql/types'; -import { - FrameworkRequest, - RequestOptions, - RequestOptionsPaginated, - RequestBasicOptions, -} from '../framework'; +import { FrameworkRequest, RequestOptions, RequestOptionsPaginated } from '../framework'; import { SearchHit } from '../types'; export interface EventsAdapter { @@ -31,10 +25,6 @@ export interface EventsAdapter { req: FrameworkRequest, options: LastEventTimeRequestOptions ): Promise<LastEventTimeData>; - getEventsOverTime( - req: FrameworkRequest, - options: RequestBasicOptions - ): Promise<EventsOverTimeData>; } export interface TimelineRequestOptions extends RequestOptions { diff --git a/x-pack/legacy/plugins/siem/server/lib/framework/types.ts b/x-pack/legacy/plugins/siem/server/lib/framework/types.ts index 9fc78e6fb84fe0..7d049d1dcd1954 100644 --- a/x-pack/legacy/plugins/siem/server/lib/framework/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/framework/types.ts @@ -17,6 +17,7 @@ import { SourceConfiguration, TimerangeInput, Maybe, + HistogramType, } from '../../graphql/types'; export * from '../../utils/typed_resolvers'; @@ -117,7 +118,8 @@ export interface RequestBasicOptions { } export interface MatrixHistogramRequestOptions extends RequestBasicOptions { - stackByField?: Maybe<string>; + stackByField: Maybe<string>; + histogramType: HistogramType; } export interface RequestOptions extends RequestBasicOptions { diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticsearch_adapter.ts new file mode 100644 index 00000000000000..f661fe165130ee --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticsearch_adapter.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; + +import { MatrixHistogramOverTimeData, HistogramType } from '../../graphql/types'; +import { inspectStringifyObject } from '../../utils/build_query'; +import { FrameworkAdapter, FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; +import { MatrixHistogramAdapter, MatrixHistogramDataConfig, MatrixHistogramHit } from './types'; +import { TermAggregation } from '../types'; +import { buildAnomaliesOverTimeQuery } from './query.anomalies_over_time.dsl'; +import { buildDnsHistogramQuery } from './query_dns_histogram.dsl'; +import { buildEventsOverTimeQuery } from './query.events_over_time.dsl'; +import { getDnsParsedData, getGenericData } from './utils'; +import { buildAuthenticationsOverTimeQuery } from './query.authentications_over_time.dsl'; +import { buildAlertsHistogramQuery } from './query_alerts.dsl'; + +const matrixHistogramConfig: MatrixHistogramDataConfig = { + [HistogramType.alerts]: { + buildDsl: buildAlertsHistogramQuery, + aggName: 'aggregations.alertsGroup.buckets', + parseKey: 'alerts.buckets', + }, + [HistogramType.anomalies]: { + buildDsl: buildAnomaliesOverTimeQuery, + aggName: 'aggregations.anomalyActionGroup.buckets', + parseKey: 'anomalies.buckets', + }, + [HistogramType.authentications]: { + buildDsl: buildAuthenticationsOverTimeQuery, + aggName: 'aggregations.eventActionGroup.buckets', + parseKey: 'events.buckets', + }, + [HistogramType.dns]: { + buildDsl: buildDnsHistogramQuery, + aggName: 'aggregations.NetworkDns.buckets', + parseKey: 'dns.buckets', + parser: getDnsParsedData, + }, + [HistogramType.events]: { + buildDsl: buildEventsOverTimeQuery, + aggName: 'aggregations.eventActionGroup.buckets', + parseKey: 'events.buckets', + }, +}; + +export class ElasticsearchMatrixHistogramAdapter implements MatrixHistogramAdapter { + constructor(private readonly framework: FrameworkAdapter) {} + + public async getHistogramData( + request: FrameworkRequest, + options: MatrixHistogramRequestOptions + ): Promise<MatrixHistogramOverTimeData> { + const myConfig = getOr(null, options.histogramType, matrixHistogramConfig); + if (myConfig == null) { + throw new Error(`This histogram type ${options.histogramType} is unknown to the server side`); + } + const dsl = myConfig.buildDsl(options); + const response = await this.framework.callWithRequest< + MatrixHistogramHit<HistogramType>, + TermAggregation + >(request, 'search', dsl); + const totalCount = getOr(0, 'hits.total.value', response); + const matrixHistogramData = getOr([], myConfig.aggName, response); + const inspect = { + dsl: [inspectStringifyObject(dsl)], + response: [inspectStringifyObject(response)], + }; + + return { + inspect, + matrixHistogramData: myConfig.parser + ? myConfig.parser<typeof options.histogramType>(matrixHistogramData, myConfig.parseKey) + : getGenericData<typeof options.histogramType>(matrixHistogramData, myConfig.parseKey), + totalCount, + }; + } +} diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticseatch_adapter.test.ts similarity index 86% rename from x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticseatch_adapter.test.ts index 210c97892e25c8..0b63785d2203bc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticseatch_adapter.test.ts @@ -6,7 +6,7 @@ import { FrameworkAdapter, FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; import expect from '@kbn/expect'; -import { ElasticsearchAlertsAdapter } from './elasticsearch_adapter'; +import { ElasticsearchMatrixHistogramAdapter } from './elasticsearch_adapter'; import { mockRequest, mockOptions, @@ -15,7 +15,7 @@ import { mockAlertsHistogramDataFormattedResponse, } from './mock'; -jest.mock('./query.dsl', () => { +jest.mock('./query_alerts.dsl', () => { return { buildAlertsHistogramQuery: jest.fn(() => mockAlertsHistogramQueryDsl), }; @@ -37,8 +37,8 @@ describe('alerts elasticsearch_adapter', () => { callWithRequest: mockCallWithRequest, })); - const EsNetworkTimelineAlerts = new ElasticsearchAlertsAdapter(mockFramework); - const data = await EsNetworkTimelineAlerts.getAlertsHistogramData( + const adapter = new ElasticsearchMatrixHistogramAdapter(mockFramework); + const data = await adapter.getHistogramData( (mockRequest as unknown) as FrameworkRequest, (mockOptions as unknown) as MatrixHistogramRequestOptions ); diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/index.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/index.ts similarity index 55% rename from x-pack/legacy/plugins/siem/server/lib/anomalies/index.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/index.ts index 727c45a3bac44e..900a6ab619ae03 100644 --- a/x-pack/legacy/plugins/siem/server/lib/anomalies/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/index.ts @@ -6,16 +6,16 @@ import { FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; export * from './elasticsearch_adapter'; -import { AnomaliesAdapter } from './types'; -import { AnomaliesOverTimeData } from '../../../public/graphql/types'; +import { MatrixHistogramAdapter } from './types'; +import { MatrixHistogramOverTimeData } from '../../graphql/types'; -export class Anomalies { - constructor(private readonly adapter: AnomaliesAdapter) {} +export class MatrixHistogram { + constructor(private readonly adapter: MatrixHistogramAdapter) {} - public async getAnomaliesOverTime( + public async getMatrixHistogramData( req: FrameworkRequest, options: MatrixHistogramRequestOptions - ): Promise<AnomaliesOverTimeData> { - return this.adapter.getAnomaliesOverTime(req, options); + ): Promise<MatrixHistogramOverTimeData> { + return this.adapter.getHistogramData(req, options); } } diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/mock.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/mock.ts similarity index 95% rename from x-pack/legacy/plugins/siem/server/lib/alerts/mock.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/mock.ts index fe0b6673f3191a..3e51e926bea875 100644 --- a/x-pack/legacy/plugins/siem/server/lib/alerts/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/mock.ts @@ -5,6 +5,7 @@ */ import { defaultIndexPattern } from '../../../default_index_pattern'; +import { HistogramType } from '../../graphql/types'; export const mockAlertsHistogramDataResponse = { took: 513, @@ -36,7 +37,7 @@ export const mockAlertsHistogramDataResponse = { hits: [], }, aggregations: { - alertsByModuleGroup: { + alertsGroup: { doc_count_error_upper_bound: 0, sum_other_doc_count: 802087, buckets: [ @@ -112,4 +113,6 @@ export const mockOptions = { }, defaultIndex: defaultIndexPattern, filterQuery: '', + stackByField: 'event.module', + histogramType: HistogramType.alerts, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/query.anomalies_over_time.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/anomalies/query.anomalies_over_time.dsl.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/query.authentications_over_time.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/authentications/query.authentications_over_time.dsl.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/events/query.events_over_time.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.events_over_time.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/events/query.events_over_time.dsl.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.events_over_time.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/query.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_alerts.dsl.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/alerts/query.dsl.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_alerts.dsl.ts index eb823271975439..4963f01d67a4f0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/alerts/query.dsl.ts +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_alerts.dsl.ts @@ -82,7 +82,7 @@ export const buildAlertsHistogramQuery = ({ }, }; return { - alertsByModuleGroup: { + alertsGroup: { terms: { field: stackByField, missing: 'All others', diff --git a/x-pack/legacy/plugins/siem/server/lib/network/query_dns_histogram.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_dns_histogram.dsl.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/network/query_dns_histogram.dsl.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_dns_histogram.dsl.ts index 1ce324e0ffff89..a6c75fe01eb151 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/query_dns_histogram.dsl.ts +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_dns_histogram.dsl.ts @@ -42,7 +42,7 @@ export const buildDnsHistogramQuery = ({ NetworkDns: { ...dateHistogram, aggs: { - histogram: { + dns: { terms: { field: stackByField, order: { diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/types.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/types.ts new file mode 100644 index 00000000000000..87ea4b81f5fba9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/types.ts @@ -0,0 +1,144 @@ +/* + * 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 { + MatrixHistogramOverTimeData, + HistogramType, + MatrixOverTimeHistogramData, +} from '../../graphql/types'; +import { FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; +import { SearchHit } from '../types'; +import { EventHit } from '../events/types'; +import { AuthenticationHit } from '../authentications/types'; + +export interface HistogramBucket { + key: number; + doc_count: number; +} + +interface AlertsGroupData { + key: string; + doc_count: number; + alerts: { + buckets: HistogramBucket[]; + }; +} + +interface AnomaliesOverTimeHistogramData { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface AnomaliesActionGroupData { + key: number; + anomalies: { + bucket: AnomaliesOverTimeHistogramData[]; + }; + doc_count: number; +} + +export interface AnomalySource { + [field: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +export interface AnomalyHit extends SearchHit { + sort: string[]; + _source: AnomalySource; + aggregations: { + [agg: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any + }; +} + +interface EventsOverTimeHistogramData { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface EventsActionGroupData { + key: number; + events: { + bucket: EventsOverTimeHistogramData[]; + }; + doc_count: number; +} + +export interface DnsHistogramSubBucket { + key: string; + doc_count: number; + orderAgg: { + value: number; + }; +} +interface DnsHistogramBucket { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; + buckets: DnsHistogramSubBucket[]; +} + +export interface DnsHistogramGroupData { + key: number; + doc_count: number; + key_as_string: string; + histogram: DnsHistogramBucket; +} + +export interface MatrixHistogramSchema<T> { + buildDsl: (options: MatrixHistogramRequestOptions) => {}; + aggName: string; + parseKey: string; + parser?: <T>( + data: MatrixHistogramParseData<T>, + keyBucket: string + ) => MatrixOverTimeHistogramData[]; +} + +export type MatrixHistogramParseData<T> = T extends HistogramType.alerts + ? AlertsGroupData[] + : T extends HistogramType.anomalies + ? AnomaliesActionGroupData[] + : T extends HistogramType.dns + ? DnsHistogramGroupData[] + : T extends HistogramType.authentications + ? AuthenticationsActionGroupData[] + : T extends HistogramType.events + ? EventsActionGroupData[] + : never; + +export type MatrixHistogramHit<T> = T extends HistogramType.alerts + ? EventHit + : T extends HistogramType.anomalies + ? AnomalyHit + : T extends HistogramType.dns + ? EventHit + : T extends HistogramType.authentications + ? AuthenticationHit + : T extends HistogramType.events + ? EventHit + : never; + +export type MatrixHistogramDataConfig = Record<HistogramType, MatrixHistogramSchema<HistogramType>>; +interface AuthenticationsOverTimeHistogramData { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface AuthenticationsActionGroupData { + key: number; + events: { + bucket: AuthenticationsOverTimeHistogramData[]; + }; + doc_count: number; +} + +export interface MatrixHistogramAdapter { + getHistogramData( + request: FrameworkRequest, + options: MatrixHistogramRequestOptions + ): Promise<MatrixHistogramOverTimeData>; +} diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/utils.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/utils.ts new file mode 100644 index 00000000000000..67568b96fee906 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/utils.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, getOr } from 'lodash/fp'; +import { MatrixHistogramParseData, DnsHistogramSubBucket, HistogramBucket } from './types'; +import { MatrixOverTimeHistogramData } from '../../graphql/types'; + +export const getDnsParsedData = <T>( + data: MatrixHistogramParseData<T>, + keyBucket: string +): MatrixOverTimeHistogramData[] => { + let result: MatrixOverTimeHistogramData[] = []; + data.forEach((bucketData: unknown) => { + const time = get('key', bucketData); + const histData = getOr([], keyBucket, bucketData).map( + ({ key, doc_count }: DnsHistogramSubBucket) => ({ + x: time, + y: doc_count, + g: key, + }) + ); + result = [...result, ...histData]; + }); + return result; +}; + +export const getGenericData = <T>( + data: MatrixHistogramParseData<T>, + keyBucket: string +): MatrixOverTimeHistogramData[] => { + let result: MatrixOverTimeHistogramData[] = []; + data.forEach((bucketData: unknown) => { + const group = get('key', bucketData); + const histData = getOr([], keyBucket, bucketData).map( + ({ key, doc_count }: HistogramBucket) => ({ + x: key, + y: doc_count, + g: group, + }) + ); + result = [...result, ...histData]; + }); + + return result; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts index 4bd980fd2ff803..39babc58ee138c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts @@ -18,16 +18,9 @@ import { NetworkHttpData, NetworkHttpEdges, NetworkTopNFlowEdges, - NetworkDsOverTimeData, - MatrixOverTimeHistogramData, } from '../../graphql/types'; import { inspectStringifyObject } from '../../utils/build_query'; -import { - DatabaseSearchResponse, - FrameworkAdapter, - FrameworkRequest, - MatrixHistogramRequestOptions, -} from '../framework'; +import { DatabaseSearchResponse, FrameworkAdapter, FrameworkRequest } from '../framework'; import { TermAggregation } from '../types'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; @@ -38,7 +31,6 @@ import { NetworkTopNFlowRequestOptions, } from './index'; import { buildDnsQuery } from './query_dns.dsl'; -import { buildDnsHistogramQuery } from './query_dns_histogram.dsl'; import { buildTopNFlowQuery, getOppositeField } from './query_top_n_flow.dsl'; import { buildHttpQuery } from './query_http.dsl'; import { buildTopCountriesQuery } from './query_top_countries.dsl'; @@ -48,9 +40,7 @@ import { NetworkTopCountriesBuckets, NetworkHttpBuckets, NetworkTopNFlowBuckets, - DnsHistogramGroupData, } from './types'; -import { EventHit } from '../events/types'; export class ElasticsearchNetworkAdapter implements NetworkAdapter { constructor(private readonly framework: FrameworkAdapter) {} @@ -202,41 +192,8 @@ export class ElasticsearchNetworkAdapter implements NetworkAdapter { totalCount, }; } - - public async getNetworkDnsHistogramData( - request: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<NetworkDsOverTimeData> { - const dsl = buildDnsHistogramQuery(options); - const response = await this.framework.callWithRequest<EventHit, TermAggregation>( - request, - 'search', - dsl - ); - const totalCount = getOr(0, 'hits.total.value', response); - const matrixHistogramData = getOr([], 'aggregations.NetworkDns.buckets', response); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - return { - inspect, - matrixHistogramData: getHistogramData(matrixHistogramData), - totalCount, - }; - } } -const getHistogramData = (data: DnsHistogramGroupData[]): MatrixOverTimeHistogramData[] => { - return data.reduce( - (acc: MatrixOverTimeHistogramData[], { key: time, histogram: { buckets } }) => { - const temp = buckets.map(({ key, doc_count }) => ({ x: time, y: doc_count, g: key })); - return [...acc, ...temp]; - }, - [] - ); -}; - const getTopNFlowEdges = ( response: DatabaseSearchResponse<NetworkTopNFlowData, TermAggregation>, options: NetworkTopNFlowRequestOptions diff --git a/x-pack/legacy/plugins/siem/server/lib/network/index.ts b/x-pack/legacy/plugins/siem/server/lib/network/index.ts index cbcd33b753d8ad..42ce9f0726ddb6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/index.ts @@ -14,13 +14,8 @@ import { NetworkTopCountriesData, NetworkTopNFlowData, NetworkTopTablesSortField, - NetworkDsOverTimeData, } from '../../graphql/types'; -import { - FrameworkRequest, - RequestOptionsPaginated, - MatrixHistogramRequestOptions, -} from '../framework'; +import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; export * from './elasticsearch_adapter'; import { NetworkAdapter } from './types'; @@ -73,13 +68,6 @@ export class Network { return this.adapter.getNetworkDns(req, options); } - public async getNetworkDnsHistogramData( - req: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<NetworkDsOverTimeData> { - return this.adapter.getNetworkDnsHistogramData(req, options); - } - public async getNetworkHttp( req: FrameworkRequest, options: NetworkHttpRequestOptions diff --git a/x-pack/legacy/plugins/siem/server/lib/network/types.ts b/x-pack/legacy/plugins/siem/server/lib/network/types.ts index b5563f9a2fef12..b7848be0971514 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/types.ts @@ -9,13 +9,8 @@ import { NetworkHttpData, NetworkTopCountriesData, NetworkTopNFlowData, - NetworkDsOverTimeData, } from '../../graphql/types'; -import { - FrameworkRequest, - RequestOptionsPaginated, - MatrixHistogramRequestOptions, -} from '../framework'; +import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; import { TotalValue } from '../types'; import { NetworkDnsRequestOptions } from '.'; @@ -29,10 +24,6 @@ export interface NetworkAdapter { options: RequestOptionsPaginated ): Promise<NetworkTopNFlowData>; getNetworkDns(req: FrameworkRequest, options: NetworkDnsRequestOptions): Promise<NetworkDnsData>; - getNetworkDnsHistogramData( - request: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<NetworkDsOverTimeData>; getNetworkHttp(req: FrameworkRequest, options: RequestOptionsPaginated): Promise<NetworkHttpData>; } diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index 34a50cf9624129..323ced734d24ba 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -8,7 +8,6 @@ import { AuthenticatedUser } from '../../../../../plugins/security/public'; import { RequestHandlerContext } from '../../../../../../src/core/server'; export { ConfigType as Configuration } from '../../../../../plugins/siem/server'; -import { Anomalies } from './anomalies'; import { Authentications } from './authentications'; import { Events } from './events'; import { FrameworkAdapter, FrameworkRequest } from './framework'; @@ -26,18 +25,17 @@ import { Note } from './note/saved_object'; import { PinnedEvent } from './pinned_event/saved_object'; import { Timeline } from './timeline/saved_object'; import { TLS } from './tls'; -import { Alerts } from './alerts'; +import { MatrixHistogram } from './matrix_histogram'; export * from './hosts'; export interface AppDomainLibs { - alerts: Alerts; - anomalies: Anomalies; authentications: Authentications; events: Events; fields: IndexFields; hosts: Hosts; ipDetails: IpDetails; + matrixHistogram: MatrixHistogram; network: Network; kpiNetwork: KpiNetwork; overview: Overview; diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index b0e10d245e0b95..d936e2a467f525 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -15,6 +15,7 @@ export const config = { ui: true, }, schema: schema.object({ + enabled: schema.boolean({ defaultValue: true }), serviceMapEnabled: schema.boolean({ defaultValue: false }), autocreateApmIndexPattern: schema.boolean({ defaultValue: true }), ui: schema.object({ diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 0128cd3dd6df7c..0dc3fc29ca8050 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -118,4 +118,4 @@ export interface EndpointMetadata { /** * The PageId type is used for the payload when firing userNavigatedToPage actions */ -export type PageId = 'alertsPage' | 'endpointListPage'; +export type PageId = 'alertsPage' | 'managementPage'; diff --git a/x-pack/plugins/endpoint/package.json b/x-pack/plugins/endpoint/package.json index 8efd0eab0eee0d..25afe2c8442ba2 100644 --- a/x-pack/plugins/endpoint/package.json +++ b/x-pack/plugins/endpoint/package.json @@ -9,6 +9,7 @@ "react-redux": "^7.1.0" }, "devDependencies": { - "@types/react-redux": "^7.1.0" + "@types/react-redux": "^7.1.0", + "redux-devtools-extension": "^2.13.8" } } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 9bea41126d2963..a86c647e771d41 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -13,6 +13,7 @@ import { Provider } from 'react-redux'; import { Store } from 'redux'; import { appStoreFactory } from './store'; import { AlertIndex } from './view/alerts'; +import { ManagementList } from './view/managing'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. @@ -20,13 +21,12 @@ import { AlertIndex } from './view/alerts'; export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMountParameters) { coreStart.http.get('/api/endpoint/hello-world'); - const [store, stopSagas] = appStoreFactory(coreStart); + const store = appStoreFactory(coreStart); ReactDOM.render(<AppRoot basename={appBasePath} store={store} />, element); return () => { ReactDOM.unmountComponentAtNode(element); - stopSagas(); }; } @@ -49,22 +49,7 @@ const AppRoot: React.FunctionComponent<RouterProps> = React.memo(({ basename, st </h1> )} /> - <Route - path="/management" - render={() => { - // FIXME: This is temporary. Will be removed in next PR for endpoint list - store.dispatch({ type: 'userEnteredEndpointListPage' }); - - return ( - <h1 data-test-subj="endpointManagement"> - <FormattedMessage - id="xpack.endpoint.endpointManagement" - defaultMessage="Manage Endpoints" - /> - </h1> - ); - }} - /> + <Route path="/management" component={ManagementList} /> <Route path="/alerts" component={AlertIndex} /> <Route render={() => ( diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts index 593041af75c05a..04c6cf7fc46340 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EndpointListAction } from './endpoint_list'; +import { ManagementAction } from './managing'; import { AlertAction } from './alerts'; import { RoutingAction } from './routing'; -export type AppAction = EndpointListAction | AlertAction | RoutingAction; +export type AppAction = ManagementAction | AlertAction | RoutingAction; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/action.ts deleted file mode 100644 index 02ec0f9d09035d..00000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/action.ts +++ /dev/null @@ -1,25 +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 { EndpointListData } from './types'; - -interface ServerReturnedEndpointList { - type: 'serverReturnedEndpointList'; - payload: EndpointListData; -} - -interface UserEnteredEndpointListPage { - type: 'userEnteredEndpointListPage'; -} - -interface UserExitedEndpointListPage { - type: 'userExitedEndpointListPage'; -} - -export type EndpointListAction = - | ServerReturnedEndpointList - | UserEnteredEndpointListPage - | UserExitedEndpointListPage; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.ts deleted file mode 100644 index bdf0708457bb06..00000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.ts +++ /dev/null @@ -1,10 +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. - */ - -export { endpointListReducer } from './reducer'; -export { EndpointListAction } from './action'; -export { endpointListSaga } from './saga'; -export * from './types'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/reducer.ts deleted file mode 100644 index e57d9683e47070..00000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/reducer.ts +++ /dev/null @@ -1,36 +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 { Reducer } from 'redux'; -import { EndpointListState } from './types'; -import { AppAction } from '../action'; - -const initialState = (): EndpointListState => { - return { - endpoints: [], - request_page_size: 10, - request_index: 0, - total: 0, - }; -}; - -export const endpointListReducer: Reducer<EndpointListState, AppAction> = ( - state = initialState(), - action -) => { - if (action.type === 'serverReturnedEndpointList') { - return { - ...state, - ...action.payload, - }; - } - - if (action.type === 'userExitedEndpointListPage') { - return initialState(); - } - - return state; -}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts deleted file mode 100644 index 6bf946873e1797..00000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts +++ /dev/null @@ -1,118 +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 { CoreStart, HttpSetup } from 'kibana/public'; -import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; -import { createSagaMiddleware, SagaContext } from '../../lib'; -import { endpointListSaga } from './saga'; -import { coreMock } from '../../../../../../../../src/core/public/mocks'; -import { - EndpointData, - EndpointListAction, - EndpointListData, - endpointListReducer, - EndpointListState, -} from './index'; -import { endpointListData } from './selectors'; - -describe('endpoint list saga', () => { - const sleep = (ms = 100) => new Promise(wakeup => setTimeout(wakeup, ms)); - let fakeCoreStart: jest.Mocked<CoreStart>; - let fakeHttpServices: jest.Mocked<HttpSetup>; - let store: Store<EndpointListState>; - let dispatch: Dispatch<EndpointListAction>; - let stopSagas: () => void; - - // TODO: consolidate the below ++ helpers in `index.test.ts` into a `test_helpers.ts`?? - const generateEndpoint = (): EndpointData => { - return { - machine_id: Math.random() - .toString(16) - .substr(2), - created_at: new Date(), - host: { - name: '', - hostname: '', - ip: '', - mac_address: '', - os: { - name: '', - full: '', - }, - }, - endpoint: { - domain: '', - is_base_image: true, - active_directory_distinguished_name: '', - active_directory_hostname: '', - upgrade: { - status: '', - updated_at: new Date(), - }, - isolation: { - status: false, - request_status: true, - updated_at: new Date(), - }, - policy: { - name: '', - id: '', - }, - sensor: { - persistence: true, - status: {}, - }, - }, - }; - }; - const getEndpointListApiResponse = (): EndpointListData => { - return { - endpoints: [generateEndpoint()], - request_page_size: 1, - request_index: 1, - total: 10, - }; - }; - - const endpointListSagaFactory = () => { - return async (sagaContext: SagaContext) => { - await endpointListSaga(sagaContext, fakeCoreStart).catch((e: Error) => { - // eslint-disable-next-line no-console - console.error(e); - return Promise.reject(e); - }); - }; - }; - - beforeEach(() => { - fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); - fakeHttpServices = fakeCoreStart.http as jest.Mocked<HttpSetup>; - - const sagaMiddleware = createSagaMiddleware(endpointListSagaFactory()); - store = createStore(endpointListReducer, applyMiddleware(sagaMiddleware)); - - sagaMiddleware.start(); - stopSagas = sagaMiddleware.stop; - dispatch = store.dispatch; - }); - - afterEach(() => { - stopSagas(); - }); - - test('it handles `userEnteredEndpointListPage`', async () => { - const apiResponse = getEndpointListApiResponse(); - - fakeHttpServices.post.mockResolvedValue(apiResponse); - expect(fakeHttpServices.post).not.toHaveBeenCalled(); - - dispatch({ type: 'userEnteredEndpointListPage' }); - await sleep(); - - expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/endpoints'); - expect(endpointListData(store.getState())).toEqual(apiResponse.endpoints); - }); -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.ts deleted file mode 100644 index cc156cfa88002b..00000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.ts +++ /dev/null @@ -1,26 +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 { CoreStart } from 'kibana/public'; -import { SagaContext } from '../../lib'; -import { EndpointListAction } from './action'; - -export const endpointListSaga = async ( - { actionsAndState, dispatch }: SagaContext<EndpointListAction>, - coreStart: CoreStart -) => { - const { post: httpPost } = coreStart.http; - - for await (const { action } of actionsAndState()) { - if (action.type === 'userEnteredEndpointListPage') { - const response = await httpPost('/api/endpoint/endpoints'); - dispatch({ - type: 'serverReturnedEndpointList', - payload: response, - }); - } - } -}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts deleted file mode 100644 index 6ffcebc3f41aac..00000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts +++ /dev/null @@ -1,9 +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 { EndpointListState } from './types'; - -export const endpointListData = (state: EndpointListState) => state.endpoints; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/types.ts deleted file mode 100644 index f2810dd89f8575..00000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/types.ts +++ /dev/null @@ -1,54 +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. - */ - -// FIXME: temporary until server defined `interface` is moved -export interface EndpointData { - machine_id: string; - created_at: Date; - host: { - name: string; - hostname: string; - ip: string; - mac_address: string; - os: { - name: string; - full: string; - }; - }; - endpoint: { - domain: string; - is_base_image: boolean; - active_directory_distinguished_name: string; - active_directory_hostname: string; - upgrade: { - status?: string; - updated_at?: Date; - }; - isolation: { - status: boolean; - request_status?: string | boolean; - updated_at?: Date; - }; - policy: { - name: string; - id: string; - }; - sensor: { - persistence: boolean; - status: object; - }; - }; -} - -// FIXME: temporary until server defined `interface` is moved to a module we can reference -export interface EndpointListData { - endpoints: EndpointData[]; - request_page_size: number; - request_index: number; - total: number; -} - -export type EndpointListState = EndpointListData; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts index a32f310392ca9e..3bbcc3f25a6d88 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts @@ -4,25 +4,62 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createStore, compose, applyMiddleware, Store } from 'redux'; +import { + createStore, + compose, + applyMiddleware, + Store, + MiddlewareAPI, + Dispatch, + Middleware, +} from 'redux'; import { CoreStart } from 'kibana/public'; -import { appSagaFactory } from './saga'; import { appReducer } from './reducer'; import { alertMiddlewareFactory } from './alerts/middleware'; +import { managementMiddlewareFactory } from './managing'; +import { GlobalState } from '../types'; +import { AppAction } from './action'; const composeWithReduxDevTools = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'EndpointApp' }) : compose; -export const appStoreFactory = (coreStart: CoreStart): [Store, () => void] => { - const sagaReduxMiddleware = appSagaFactory(coreStart); +export type Selector<S, R> = (state: S) => R; + +/** + * Wrap Redux Middleware and adjust 'getState()' to return the namespace from 'GlobalState that applies to the given Middleware concern. + * + * @param selector + * @param middleware + */ +export const substateMiddlewareFactory = <Substate>( + selector: Selector<GlobalState, Substate>, + middleware: Middleware<{}, Substate, Dispatch<AppAction>> +): Middleware<{}, GlobalState, Dispatch<AppAction>> => { + return api => { + const substateAPI: MiddlewareAPI<Dispatch<AppAction>, Substate> = { + ...api, + getState() { + return selector(api.getState()); + }, + }; + return middleware(substateAPI); + }; +}; + +export const appStoreFactory = (coreStart: CoreStart): Store => { const store = createStore( appReducer, composeWithReduxDevTools( - applyMiddleware(alertMiddlewareFactory(coreStart), appSagaFactory(coreStart)) + applyMiddleware( + alertMiddlewareFactory(coreStart), + substateMiddlewareFactory( + globalState => globalState.managementList, + managementMiddlewareFactory(coreStart) + ) + ) ) ); - sagaReduxMiddleware.start(); - return [store, sagaReduxMiddleware.stop]; + return store; }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts new file mode 100644 index 00000000000000..e916dc66c59f0f --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ManagementListPagination } from '../../types'; +import { EndpointResultList } from '../../../../../common/types'; + +interface ServerReturnedManagementList { + type: 'serverReturnedManagementList'; + payload: EndpointResultList; +} + +interface UserExitedManagementList { + type: 'userExitedManagementList'; +} + +interface UserPaginatedManagementList { + type: 'userPaginatedManagementList'; + payload: ManagementListPagination; +} + +export type ManagementAction = + | ServerReturnedManagementList + | UserExitedManagementList + | UserPaginatedManagementList; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts similarity index 51% rename from x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.test.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts index a46653f82ee459..dde0ba1e96a8ab 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts @@ -5,64 +5,52 @@ */ import { createStore, Dispatch, Store } from 'redux'; -import { EndpointListAction, EndpointData, endpointListReducer, EndpointListState } from './index'; -import { endpointListData } from './selectors'; +import { ManagementAction, managementListReducer } from './index'; +import { EndpointMetadata } from '../../../../../common/types'; +import { ManagementListState } from '../../types'; +import { listData } from './selectors'; describe('endpoint_list store concerns', () => { - let store: Store<EndpointListState>; - let dispatch: Dispatch<EndpointListAction>; + let store: Store<ManagementListState>; + let dispatch: Dispatch<ManagementAction>; const createTestStore = () => { - store = createStore(endpointListReducer); + store = createStore(managementListReducer); dispatch = store.dispatch; }; - const generateEndpoint = (): EndpointData => { + const generateEndpoint = (): EndpointMetadata => { return { - machine_id: Math.random() - .toString(16) - .substr(2), - created_at: new Date(), - host: { - name: '', - hostname: '', - ip: '', - mac_address: '', - os: { - name: '', - full: '', - }, + event: { + created: new Date(0), }, endpoint: { - domain: '', - is_base_image: true, - active_directory_distinguished_name: '', - active_directory_hostname: '', - upgrade: { - status: '', - updated_at: new Date(), - }, - isolation: { - status: false, - request_status: true, - updated_at: new Date(), - }, policy: { - name: '', id: '', }, - sensor: { - persistence: true, - status: {}, + }, + agent: { + version: '', + id: '', + }, + host: { + id: '', + hostname: '', + ip: [''], + mac: [''], + os: { + name: '', + full: '', + version: '', }, }, }; }; const loadDataToStore = () => { dispatch({ - type: 'serverReturnedEndpointList', + type: 'serverReturnedManagementList', payload: { endpoints: [generateEndpoint()], request_page_size: 1, - request_index: 1, + request_page_index: 1, total: 10, }, }); @@ -76,39 +64,40 @@ describe('endpoint_list store concerns', () => { test('it creates default state', () => { expect(store.getState()).toEqual({ endpoints: [], - request_page_size: 10, - request_index: 0, + pageSize: 10, + pageIndex: 0, total: 0, + loading: false, }); }); - test('it handles `serverReturnedEndpointList', () => { + test('it handles `serverReturnedManagementList', () => { const payload = { endpoints: [generateEndpoint()], request_page_size: 1, - request_index: 1, + request_page_index: 1, total: 10, }; dispatch({ - type: 'serverReturnedEndpointList', + type: 'serverReturnedManagementList', payload, }); const currentState = store.getState(); expect(currentState.endpoints).toEqual(payload.endpoints); - expect(currentState.request_page_size).toEqual(payload.request_page_size); - expect(currentState.request_index).toEqual(payload.request_index); + expect(currentState.pageSize).toEqual(payload.request_page_size); + expect(currentState.pageIndex).toEqual(payload.request_page_index); expect(currentState.total).toEqual(payload.total); }); - test('it handles `userExitedEndpointListPage`', () => { + test('it handles `userExitedManagementListPage`', () => { loadDataToStore(); expect(store.getState().total).toEqual(10); - dispatch({ type: 'userExitedEndpointListPage' }); + dispatch({ type: 'userExitedManagementList' }); expect(store.getState().endpoints.length).toEqual(0); - expect(store.getState().request_index).toEqual(0); + expect(store.getState().pageIndex).toEqual(0); }); }); @@ -118,9 +107,9 @@ describe('endpoint_list store concerns', () => { loadDataToStore(); }); - test('it selects `endpointListData`', () => { + test('it selects `managementListData`', () => { const currentState = store.getState(); - expect(endpointListData(currentState)).toEqual(currentState.endpoints); + expect(listData(currentState)).toEqual(currentState.endpoints); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/graphql/anomalies/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.ts similarity index 60% rename from x-pack/legacy/plugins/siem/server/graphql/anomalies/index.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.ts index 4bfd6be173105d..f0bfe27c9e30f4 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/anomalies/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { createAnomaliesResolvers } from './resolvers'; -export { anomaliesSchema } from './schema.gql'; +export { managementListReducer } from './reducer'; +export { ManagementAction } from './action'; +export { managementMiddlewareFactory } from './middleware'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts new file mode 100644 index 00000000000000..095e49a6c4306a --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { CoreStart, HttpSetup } from 'kibana/public'; +import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { managementListReducer, managementMiddlewareFactory } from './index'; +import { EndpointMetadata, EndpointResultList } from '../../../../../common/types'; +import { ManagementListState } from '../../types'; +import { AppAction } from '../action'; +import { listData } from './selectors'; +describe('endpoint list saga', () => { + const sleep = (ms = 100) => new Promise(wakeup => setTimeout(wakeup, ms)); + let fakeCoreStart: jest.Mocked<CoreStart>; + let fakeHttpServices: jest.Mocked<HttpSetup>; + let store: Store<ManagementListState>; + let getState: typeof store['getState']; + let dispatch: Dispatch<AppAction>; + // https://github.com/elastic/endpoint-app-team/issues/131 + const generateEndpoint = (): EndpointMetadata => { + return { + event: { + created: new Date(0), + }, + endpoint: { + policy: { + id: '', + }, + }, + agent: { + version: '', + id: '', + }, + host: { + id: '', + hostname: '', + ip: [''], + mac: [''], + os: { + name: '', + full: '', + version: '', + }, + }, + }; + }; + const getEndpointListApiResponse = (): EndpointResultList => { + return { + endpoints: [generateEndpoint()], + request_page_size: 1, + request_page_index: 1, + total: 10, + }; + }; + beforeEach(() => { + fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); + fakeHttpServices = fakeCoreStart.http as jest.Mocked<HttpSetup>; + store = createStore( + managementListReducer, + applyMiddleware(managementMiddlewareFactory(fakeCoreStart)) + ); + getState = store.getState; + dispatch = store.dispatch; + }); + test('it handles `userNavigatedToPage`', async () => { + const apiResponse = getEndpointListApiResponse(); + fakeHttpServices.post.mockResolvedValue(apiResponse); + expect(fakeHttpServices.post).not.toHaveBeenCalled(); + dispatch({ type: 'userNavigatedToPage', payload: 'managementPage' }); + await sleep(); + expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/endpoints', { + body: JSON.stringify({ + paging_properties: [{ page_index: 0 }, { page_size: 10 }], + }), + }); + expect(listData(getState())).toEqual(apiResponse.endpoints); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts new file mode 100644 index 00000000000000..ae756caf5aa353 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MiddlewareFactory } from '../../types'; +import { pageIndex, pageSize } from './selectors'; +import { ManagementListState } from '../../types'; +import { AppAction } from '../action'; + +export const managementMiddlewareFactory: MiddlewareFactory<ManagementListState> = coreStart => { + return ({ getState, dispatch }) => next => async (action: AppAction) => { + next(action); + if ( + (action.type === 'userNavigatedToPage' && action.payload === 'managementPage') || + action.type === 'userPaginatedManagementList' + ) { + const managementPageIndex = pageIndex(getState()); + const managementPageSize = pageSize(getState()); + const response = await coreStart.http.post('/api/endpoint/endpoints', { + body: JSON.stringify({ + paging_properties: [ + { page_index: managementPageIndex }, + { page_size: managementPageSize }, + ], + }), + }); + response.request_page_index = managementPageIndex; + dispatch({ + type: 'serverReturnedManagementList', + payload: response, + }); + } + }; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts new file mode 100644 index 00000000000000..bbbbdc4d17ce60 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Reducer } from 'redux'; +import { ManagementListState } from '../../types'; +import { AppAction } from '../action'; + +const initialState = (): ManagementListState => { + return { + endpoints: [], + pageSize: 10, + pageIndex: 0, + total: 0, + loading: false, + }; +}; + +export const managementListReducer: Reducer<ManagementListState, AppAction> = ( + state = initialState(), + action +) => { + if (action.type === 'serverReturnedManagementList') { + const { + endpoints, + total, + request_page_size: pageSize, + request_page_index: pageIndex, + } = action.payload; + return { + ...state, + endpoints, + total, + pageSize, + pageIndex, + loading: false, + }; + } + + if (action.type === 'userExitedManagementList') { + return initialState(); + } + + if (action.type === 'userPaginatedManagementList') { + return { + ...state, + ...action.payload, + loading: true, + }; + } + + return state; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts new file mode 100644 index 00000000000000..3dcb144c2bade5 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ManagementListState } from '../../types'; + +export const listData = (state: ManagementListState) => state.endpoints; + +export const pageIndex = (state: ManagementListState) => state.pageIndex; + +export const pageSize = (state: ManagementListState) => state.pageSize; + +export const totalHits = (state: ManagementListState) => state.total; + +export const isLoading = (state: ManagementListState) => state.loading; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts index a9cf6d9980519f..7d738c266fae0c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { combineReducers, Reducer } from 'redux'; -import { endpointListReducer } from './endpoint_list'; +import { managementListReducer } from './managing'; import { AppAction } from './action'; import { alertListReducer } from './alerts'; import { GlobalState } from '../types'; export const appReducer: Reducer<GlobalState, AppAction> = combineReducers({ - endpointList: endpointListReducer, + managementList: managementListReducer, alertList: alertListReducer, }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts deleted file mode 100644 index 3b7de79d5443c9..00000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts +++ /dev/null @@ -1,18 +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 { CoreStart } from 'kibana/public'; -import { createSagaMiddleware, SagaContext } from '../lib'; -import { endpointListSaga } from './endpoint_list'; - -export const appSagaFactory = (coreStart: CoreStart) => { - return createSagaMiddleware(async (sagaContext: SagaContext) => { - await Promise.all([ - // Concerns specific sagas here - endpointListSaga(sagaContext, coreStart), - ]); - }); -}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index 5f02d36308053d..02a7793fc38b06 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -6,20 +6,42 @@ import { Dispatch, MiddlewareAPI } from 'redux'; import { CoreStart } from 'kibana/public'; -import { EndpointListState } from './store/endpoint_list'; +import { EndpointMetadata } from '../../../common/types'; import { AppAction } from './store/action'; import { AlertResultList } from '../../../common/types'; -export type MiddlewareFactory = ( +export type MiddlewareFactory<S = GlobalState> = ( coreStart: CoreStart ) => ( - api: MiddlewareAPI<Dispatch<AppAction>, GlobalState> + api: MiddlewareAPI<Dispatch<AppAction>, S> ) => (next: Dispatch<AppAction>) => (action: AppAction) => unknown; +export interface ManagementListState { + endpoints: EndpointMetadata[]; + total: number; + pageSize: number; + pageIndex: number; + loading: boolean; +} + +export interface ManagementListPagination { + pageIndex: number; + pageSize: number; +} + export interface GlobalState { - readonly endpointList: EndpointListState; + readonly managementList: ManagementListState; readonly alertList: AlertListState; } export type AlertListData = AlertResultList; export type AlertListState = AlertResultList; +export type CreateStructuredSelector = < + SelectorMap extends { [key: string]: (...args: never[]) => unknown } +>( + selectorMap: SelectorMap +) => ( + state: SelectorMap[keyof SelectorMap] extends (state: infer State) => unknown ? State : never +) => { + [Key in keyof SelectorMap]: ReturnType<SelectorMap[Key]>; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/hooks.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/hooks.ts new file mode 100644 index 00000000000000..a0720fbd8aeeb2 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/hooks.ts @@ -0,0 +1,16 @@ +/* + * 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 { useSelector } from 'react-redux'; +import { GlobalState, ManagementListState } from '../../types'; + +export function useManagementListSelector<TSelected>( + selector: (state: ManagementListState) => TSelected +) { + return useSelector(function(state: GlobalState) { + return selector(state.managementList); + }); +} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx new file mode 100644 index 00000000000000..44b08f25c76535 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx @@ -0,0 +1,167 @@ +/* + * 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, { useMemo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiTitle, + EuiBasicTable, + EuiTextColor, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { createStructuredSelector } from 'reselect'; +import * as selectors from '../../store/managing/selectors'; +import { ManagementAction } from '../../store/managing/action'; +import { useManagementListSelector } from './hooks'; +import { usePageId } from '../use_page_id'; +import { CreateStructuredSelector } from '../../types'; + +const selector = (createStructuredSelector as CreateStructuredSelector)(selectors); +export const ManagementList = () => { + usePageId('managementPage'); + const dispatch = useDispatch<(a: ManagementAction) => void>(); + const { + listData, + pageIndex, + pageSize, + totalHits: totalItemCount, + isLoading, + } = useManagementListSelector(selector); + + const paginationSetup = useMemo(() => { + return { + pageIndex, + pageSize, + totalItemCount, + pageSizeOptions: [10, 20, 50], + hidePerPageOptions: false, + }; + }, [pageIndex, pageSize, totalItemCount]); + + const onTableChange = useCallback( + ({ page }: { page: { index: number; size: number } }) => { + const { index, size } = page; + dispatch({ + type: 'userPaginatedManagementList', + payload: { pageIndex: index, pageSize: size }, + }); + }, + [dispatch] + ); + + const columns = [ + { + field: 'host.hostname', + name: i18n.translate('xpack.endpoint.management.list.host', { + defaultMessage: 'Hostname', + }), + }, + { + field: '', + name: i18n.translate('xpack.endpoint.management.list.policy', { + defaultMessage: 'Policy', + }), + render: () => { + return 'Policy Name'; + }, + }, + { + field: '', + name: i18n.translate('xpack.endpoint.management.list.policyStatus', { + defaultMessage: 'Policy Status', + }), + render: () => { + return 'Policy Status'; + }, + }, + { + field: '', + name: i18n.translate('xpack.endpoint.management.list.alerts', { + defaultMessage: 'Alerts', + }), + render: () => { + return '0'; + }, + }, + { + field: 'host.os.name', + name: i18n.translate('xpack.endpoint.management.list.os', { + defaultMessage: 'Operating System', + }), + }, + { + field: 'host.ip', + name: i18n.translate('xpack.endpoint.management.list.ip', { + defaultMessage: 'IP Address', + }), + }, + { + field: '', + name: i18n.translate('xpack.endpoint.management.list.sensorVersion', { + defaultMessage: 'Sensor Version', + }), + render: () => { + return 'version'; + }, + }, + { + field: '', + name: i18n.translate('xpack.endpoint.management.list.lastActive', { + defaultMessage: 'Last Active', + }), + render: () => { + return 'xxxx'; + }, + }, + ]; + + return ( + <EuiPage> + <EuiPageBody> + <EuiPageContent> + <EuiPageContentHeader> + <EuiPageContentHeaderSection> + <EuiTitle> + <h2 data-test-subj="managementViewTitle"> + <FormattedMessage + id="xpack.endpoint.managementList.hosts" + defaultMessage="Hosts" + /> + </h2> + </EuiTitle> + <h4> + <EuiTextColor color="subdued"> + <FormattedMessage + id="xpack.endpoint.managementList.totalCount" + defaultMessage="{totalItemCount} Hosts" + values={{ totalItemCount }} + /> + </EuiTextColor> + </h4> + </EuiPageContentHeaderSection> + </EuiPageContentHeader> + <EuiPageContentBody> + <EuiBasicTable + data-test-subj="managementListTable" + items={listData} + columns={columns} + loading={isLoading} + pagination={paginationSetup} + onChange={onTableChange} + /> + </EuiPageContentBody> + </EuiPageContent> + </EuiPageBody> + </EuiPage> + ); +}; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts deleted file mode 100644 index c7f790588a739e..00000000000000 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts +++ /dev/null @@ -1,9 +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 { CameraAction } from './store/camera'; -import { DataAction } from './store/data'; - -export type ResolverAction = CameraAction | DataAction; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/documentation/camera.md b/x-pack/plugins/endpoint/public/embeddables/resolver/documentation/camera.md new file mode 100644 index 00000000000000..aeca76fad916f7 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/documentation/camera.md @@ -0,0 +1,26 @@ +# Introduction + +Resolver renders a map in a DOM element. Items on the map are placed in 2 dimensions using arbitrary units. Like other mapping software, the map can show things at different scales. The 'camera' determines what is shown on the map. + +The camera is positioned. When the user clicks-and-drags the map, the camera's position is changed. This allows the user to pan around the map and see things that would otherwise be out of view, at a given scale. + +The camera determines the scale. If the scale is smaller, the viewport of the map is larger and more is visible. This allows the user to zoom in an out. On screen controls and gestures (trackpad-pinch, or CTRL-mousewheel) change the scale. + +# Concepts + +## Scaling +The camera scale is controlled both by the user and programatically by Resolver. There is a maximum and minimum scale value (at the time of this writing they are 0.5 and 6.) This means that the map, and things on the map, will be rendered at between 0.5 and 6 times their instrinsic dimensions. + +A range control is provided so that the user can change the scale. The user can also pinch-to-zoom on Mac OS X (or use ctrl-mousewheel otherwise) to change the scale. These interactions change the `scalingFactor`. This number is between 0 and 1. It represents how zoomed-in things should be. When the `scalingFactor` is 1, the scale will be the maximum scale value. When `scalingFactor` is 0, the scale will be the minimum scale value. Otherwise we interpolate between the minimum and maximum scale factor. The rate that the scale increases between the two is controlled by `scalingFactor**zoomCurveRate` The zoom curve rate is 4 at the time of this writing. This makes it so that the change in scale is more pronounced when the user is zoomed in. + +``` +renderScale = minimumScale * (1 - scalingFactor**curveRate) + maximumScale * scalingFactor**curveRate; +``` + +## Panning +When the user clicks and drags the map, the camera is 'moved' around. This allows the user to see different things on the map. The on-screen controls provide 4 directional buttons which nudge the camera, as well as a reset button. The reset button brings the camera back where it started (0, 0). + +Resolver may programatically change the position of the camera in order to bring some interesting elements into view. + +## Animation +The camera can animate changes to its position. Animations usually have a short, fixed duration, such as 1 second. If the camera is moving a great deal during the animation, then things could end up moving across the screen too quickly. In this case, looking at Resolver might be disorienting. In order to combat this, Resolver may temporarily decrease the scale. By decreasing the scale, objects look futher away. Far away objects appear to move slower. diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx index 9539162f9cfb64..6680ba615e3530 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx @@ -6,7 +6,8 @@ import ReactDOM from 'react-dom'; import React from 'react'; -import { AppRoot } from './view'; +import { Provider } from 'react-redux'; +import { Resolver } from './view'; import { storeFactory } from './store'; import { Embeddable } from '../../../../../../src/plugins/embeddable/public'; @@ -20,7 +21,12 @@ export class ResolverEmbeddable extends Embeddable { } this.lastRenderTarget = node; const { store } = storeFactory(); - ReactDOM.render(<AppRoot store={store} />, node); + ReactDOM.render( + <Provider store={store}> + <Resolver /> + </Provider>, + node + ); } public reload(): void { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts index c59db31c39e827..6bf0fedc84dfe8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts @@ -10,3 +10,10 @@ export function clamp(value: number, minimum: number, maximum: number) { return Math.max(Math.min(value, maximum), minimum); } + +/** + * linearly interpolate between `a` and `b` at a ratio of `ratio`. If `ratio` is `0`, return `a`, if ratio is `1`, return `b`. + */ +export function lerp(a: number, b: number, ratio: number): number { + return a * (1 - ratio) + b * ratio; +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts index 3084ce0eacdb4c..bd7d1bf743df81 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts @@ -15,11 +15,32 @@ export function inverseOrthographicProjection( bottom: number, left: number ): Matrix3 { - const m11 = (right - left) / 2; - const m13 = (right + left) / (right - left); + let m11: number; + let m13: number; + let m22: number; + let m23: number; - const m22 = (top - bottom) / 2; - const m23 = (top + bottom) / (top - bottom); + /** + * If `right - left` is 0, the width is 0, so scale everything to 0 + */ + if (right - left === 0) { + m11 = 0; + m13 = 0; + } else { + m11 = (right - left) / 2; + m13 = (right + left) / (right - left); + } + + /** + * If `top - bottom` is 0, the height is 0, so scale everything to 0 + */ + if (top - bottom === 0) { + m22 = 0; + m23 = 0; + } else { + m22 = (top - bottom) / 2; + m23 = (top + bottom) / (top - bottom); + } return [m11, 0, m13, 0, m22, m23, 0, 0, 0]; } @@ -37,11 +58,32 @@ export function orthographicProjection( bottom: number, left: number ): Matrix3 { - const m11 = 2 / (right - left); // adjust x scale to match ndc (-1, 1) bounds - const m13 = -((right + left) / (right - left)); + let m11: number; + let m13: number; + let m22: number; + let m23: number; + + /** + * If `right - left` is 0, the width is 0, so scale everything to 0 + */ + if (right - left === 0) { + m11 = 0; + m13 = 0; + } else { + m11 = 2 / (right - left); // adjust x scale to match ndc (-1, 1) bounds + m13 = -((right + left) / (right - left)); + } - const m22 = 2 / (top - bottom); // adjust y scale to match ndc (-1, 1) bounds - const m23 = -((top + bottom) / (top - bottom)); + /** + * If `top - bottom` is 0, the height is 0, so scale everything to 0 + */ + if (top - bottom === 0) { + m22 = 0; + m23 = 0; + } else { + m22 = top - bottom === 0 ? 0 : 2 / (top - bottom); // adjust y scale to match ndc (-1, 1) bounds + m23 = top - bottom === 0 ? 0 : -((top + bottom) / (top - bottom)); + } return [m11, 0, m13, 0, m22, m23, 0, 0, 0]; } @@ -68,6 +110,6 @@ export function translationTransformation([x, y]: Vector2): Matrix3 { return [ 1, 0, x, 0, 1, y, - 0, 0, 1 + 0, 0, 0 ] } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts index 3c0681413305e2..898ce6f6bacd23 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts @@ -26,6 +26,13 @@ export function divide(a: Vector2, b: Vector2): Vector2 { return [a[0] / b[0], a[1] / b[1]]; } +/** + * Return `[ a[0] * b[0], a[1] * b[1] ]` + */ +export function multiply(a: Vector2, b: Vector2): Vector2 { + return [a[0] * b[0], a[1] * b[1]]; +} + /** * Returns a vector which is the result of applying a 2D transformation matrix to the provided vector. */ @@ -50,3 +57,33 @@ export function angle(a: Vector2, b: Vector2) { const deltaY = b[1] - a[1]; return Math.atan2(deltaY, deltaX); } + +/** + * Clamp `vector`'s components. + */ +export function clamp([x, y]: Vector2, [minX, minY]: Vector2, [maxX, maxY]: Vector2): Vector2 { + return [Math.max(minX, Math.min(maxX, x)), Math.max(minY, Math.min(maxY, y))]; +} + +/** + * Scale vector by number + */ +export function scale(a: Vector2, n: number): Vector2 { + return [a[0] * n, a[1] * n]; +} + +/** + * Linearly interpolate between `a` and `b`. + * `t` represents progress and: + * 0 <= `t` <= 1 + */ +export function lerp(a: Vector2, b: Vector2, t: number): Vector2 { + return add(scale(a, 1 - t), scale(b, t)); +} + +/** + * The length of the vector + */ +export function length([x, y]: Vector2): number { + return Math.sqrt(x * x + y * y); +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts index 67acdbd253f65a..9a6f19adcc1017 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts @@ -25,6 +25,7 @@ export function mockProcessEvent( machine_id: '', ...parts, data_buffer: { + timestamp_utc: '2019-09-24 01:47:47Z', event_subtype_full: 'creation_event', event_type_full: 'process_event', process_name: '', diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts new file mode 100644 index 00000000000000..25f196c76a2904 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ProcessEvent } from '../types'; +import { CameraAction } from './camera'; +import { DataAction } from './data'; + +/** + * When the user wants to bring a process node front-and-center on the map. + */ +interface UserBroughtProcessIntoView { + readonly type: 'userBroughtProcessIntoView'; + readonly payload: { + /** + * Used to identify the process node that should be brought into view. + */ + readonly process: ProcessEvent; + /** + * The time (since epoch in milliseconds) when the action was dispatched. + */ + readonly time: number; + }; +} + +export type ResolverAction = CameraAction | DataAction | UserBroughtProcessIntoView; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts index 7d3e64ab34f231..dcc6c2c9c94111 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Vector2, PanDirection } from '../../types'; +import { Vector2 } from '../../types'; + +interface TimestampedPayload { + /** + * Time (since epoch in milliseconds) when this action was dispatched. + */ + readonly time: number; +} interface UserSetZoomLevel { readonly type: 'userSetZoomLevel'; @@ -24,11 +31,13 @@ interface UserClickedZoomIn { interface UserZoomed { readonly type: 'userZoomed'; - /** - * A value to zoom in by. Should be a fraction of `1`. For a `'wheel'` event when `event.deltaMode` is `'pixel'`, - * pass `event.deltaY / -renderHeight` where `renderHeight` is the height of the Resolver element in pixels. - */ - readonly payload: number; + readonly payload: { + /** + * A value to zoom in by. Should be a fraction of `1`. For a `'wheel'` event when `event.deltaMode` is `'pixel'`, + * pass `event.deltaY / -renderHeight` where `renderHeight` is the height of the Resolver element in pixels. + */ + readonly zoomChange: number; + } & TimestampedPayload; } interface UserSetRasterSize { @@ -40,7 +49,7 @@ interface UserSetRasterSize { } /** - * This is currently only used in tests. The 'back to center' button will use this action, and more tests around its behavior will need to be added. + * When the user warps the camera to an exact point instantly. */ interface UserSetPositionOfCamera { readonly type: 'userSetPositionOfCamera'; @@ -52,33 +61,45 @@ interface UserSetPositionOfCamera { interface UserStartedPanning { readonly type: 'userStartedPanning'; - /** - * A vector in screen coordinates (each unit is a pixel and the Y axis increases towards the bottom of the screen) - * relative to the Resolver component. - * Represents a starting position during panning for a pointing device. - */ - readonly payload: Vector2; + + readonly payload: { + /** + * A vector in screen coordinates (each unit is a pixel and the Y axis increases towards the bottom of the screen) + * relative to the Resolver component. + * Represents a starting position during panning for a pointing device. + */ + readonly screenCoordinates: Vector2; + } & TimestampedPayload; } interface UserStoppedPanning { readonly type: 'userStoppedPanning'; + + readonly payload: TimestampedPayload; } -interface UserClickedPanControl { - readonly type: 'userClickedPanControl'; +interface UserNudgedCamera { + readonly type: 'userNudgedCamera'; /** * String that represents the direction in which Resolver can be panned */ - readonly payload: PanDirection; + readonly payload: { + /** + * A cardinal direction to move the users perspective in. + */ + readonly direction: Vector2; + } & TimestampedPayload; } interface UserMovedPointer { readonly type: 'userMovedPointer'; - /** - * A vector in screen coordinates relative to the Resolver component. - * The payload should be contain clientX and clientY minus the client position of the Resolver component. - */ - readonly payload: Vector2; + readonly payload: { + /** + * A vector in screen coordinates relative to the Resolver component. + * The payload should be contain clientX and clientY minus the client position of the Resolver component. + */ + screenCoordinates: Vector2; + } & TimestampedPayload; } export type CameraAction = @@ -91,4 +112,4 @@ export type CameraAction = | UserMovedPointer | UserClickedZoomOut | UserClickedZoomIn - | UserClickedPanControl; + | UserNudgedCamera; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/animation.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/animation.test.ts new file mode 100644 index 00000000000000..795344d8af0925 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/animation.test.ts @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createStore, Store, Reducer } from 'redux'; +import { cameraReducer, cameraInitialState } from './reducer'; +import { CameraState, Vector2, ResolverAction } from '../../types'; +import * as selectors from './selectors'; +import { animatePanning } from './methods'; +import { lerp } from '../../lib/math'; + +type TestAction = + | ResolverAction + | { + readonly type: 'animatePanning'; + readonly payload: { + /** + * The start time of the animation. + */ + readonly time: number; + /** + * The duration of the animation. + */ + readonly duration: number; + /** + * The target translation the camera will animate towards. + */ + readonly targetTranslation: Vector2; + }; + }; + +describe('when the camera is created', () => { + let store: Store<CameraState, TestAction>; + beforeEach(() => { + const testReducer: Reducer<CameraState, TestAction> = ( + state = cameraInitialState(), + action + ): CameraState => { + // If the test action is fired, call the animatePanning method + if (action.type === 'animatePanning') { + const { + payload: { time, targetTranslation, duration }, + } = action; + return animatePanning(state, time, targetTranslation, duration); + } + return cameraReducer(state, action); + }; + store = createStore(testReducer); + }); + it('should be at 0,0', () => { + expect(selectors.translation(store.getState())(0)).toEqual([0, 0]); + }); + it('should have scale of [1,1]', () => { + expect(selectors.scale(store.getState())(0)).toEqual([1, 1]); + }); + describe('when animation begins', () => { + const duration = 1000; + let targetTranslation: Vector2; + const startTime = 0; + beforeEach(() => { + // The distance the camera moves must be nontrivial in order to trigger a scale animation + targetTranslation = [1000, 1000]; + const action: TestAction = { + type: 'animatePanning', + payload: { + time: startTime, + duration, + targetTranslation, + }, + }; + store.dispatch(action); + }); + describe('when the animation is in progress', () => { + let translationAtIntervals: Vector2[]; + let scaleAtIntervals: Vector2[]; + beforeEach(() => { + translationAtIntervals = []; + scaleAtIntervals = []; + const state = store.getState(); + for (let progress = 0; progress <= 1; progress += 0.1) { + translationAtIntervals.push( + selectors.translation(state)(lerp(startTime, startTime + duration, progress)) + ); + scaleAtIntervals.push( + selectors.scale(state)(lerp(startTime, startTime + duration, progress)) + ); + } + }); + it('should gradually translate to the target', () => { + expect(translationAtIntervals).toMatchInlineSnapshot(` + Array [ + Array [ + 0, + 0, + ], + Array [ + 4.000000000000001, + 4.000000000000001, + ], + Array [ + 32.00000000000001, + 32.00000000000001, + ], + Array [ + 108.00000000000004, + 108.00000000000004, + ], + Array [ + 256.00000000000006, + 256.00000000000006, + ], + Array [ + 500, + 500, + ], + Array [ + 744, + 744, + ], + Array [ + 891.9999999999999, + 891.9999999999999, + ], + Array [ + 968, + 968, + ], + Array [ + 996, + 996, + ], + Array [ + 1000, + 1000, + ], + ] + `); + }); + it('should gradually zoom in and out to the target', () => { + expect(scaleAtIntervals).toMatchInlineSnapshot(` + Array [ + Array [ + 1, + 1, + ], + Array [ + 0.9873589660765236, + 0.9873589660765236, + ], + Array [ + 0.8988717286121894, + 0.8988717286121894, + ], + Array [ + 0.7060959612791753, + 0.7060959612791753, + ], + Array [ + 0.6176087238148411, + 0.6176087238148411, + ], + Array [ + 0.6049676898913647, + 0.6049676898913647, + ], + Array [ + 0.6176087238148411, + 0.6176087238148411, + ], + Array [ + 0.7060959612791753, + 0.7060959612791753, + ], + Array [ + 0.8988717286121893, + 0.8988717286121893, + ], + Array [ + 0.9873589660765237, + 0.9873589660765237, + ], + Array [ + 1, + 1, + ], + ] + `); + }); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts index 41e3bc025f557a..000dbb8d52841d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts @@ -18,14 +18,27 @@ describe('inverseProjectionMatrix', () => { beforeEach(() => { store = createStore(cameraReducer, undefined); compare = (rasterPosition: [number, number], expectedWorldPosition: [number, number]) => { + // time isn't really relevant as we aren't testing animation + const time = 0; const [worldX, worldY] = applyMatrix3( rasterPosition, - inverseProjectionMatrix(store.getState()) + inverseProjectionMatrix(store.getState())(time) ); expect(worldX).toBeCloseTo(expectedWorldPosition[0]); expect(worldY).toBeCloseTo(expectedWorldPosition[1]); }; }); + + describe('when the raster size is 0x0 pixels', () => { + beforeEach(() => { + const action: CameraAction = { type: 'userSetRasterSize', payload: [0, 0] }; + store.dispatch(action); + }); + it('should convert 0,0 in raster space to 0,0 (center) in world space', () => { + compare([10, 0], [0, 0]); + }); + }); + describe('when the raster size is 300 x 200 pixels', () => { beforeEach(() => { const action: CameraAction = { type: 'userSetRasterSize', payload: [300, 200] }; @@ -69,7 +82,7 @@ describe('inverseProjectionMatrix', () => { }); describe('when the user has panned to the right and up by 50', () => { beforeEach(() => { - const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-50, -50] }; + const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [50, 50] }; store.dispatch(action); }); it('should convert 100,150 in raster space to 0,0 (center) in world space', () => { @@ -84,7 +97,7 @@ describe('inverseProjectionMatrix', () => { }); describe('when the user has panned to the right by 350 and up by 250', () => { beforeEach(() => { - const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-350, -250] }; + const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [350, 250] }; store.dispatch(action); }); describe('when the user has scaled to 2', () => { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/methods.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/methods.ts new file mode 100644 index 00000000000000..4afbacb819b1a4 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/methods.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { translation } from './selectors'; +import { CameraState, Vector2 } from '../../types'; + +/** + * Return a new `CameraState` with the `animation` property + * set. The camera will animate to `targetTranslation` over `duration`. + */ +export function animatePanning( + state: CameraState, + startTime: number, + targetTranslation: Vector2, + duration: number +): CameraState { + const nextState: CameraState = { + ...state, + /** + * This cancels panning if any was taking place. + */ + panning: undefined, + translationNotCountingCurrentPanning: targetTranslation, + animation: { + startTime, + targetTranslation, + initialTranslation: translation(state)(startTime), + duration, + }, + }; + + return nextState; +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts index 17401a63b5ae8f..9a9a5ea1c0cfc8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts @@ -13,11 +13,14 @@ import { translation } from './selectors'; describe('panning interaction', () => { let store: Store<CameraState, CameraAction>; let translationShouldBeCloseTo: (expectedTranslation: Vector2) => void; + let time: number; beforeEach(() => { + // The time isn't relevant as we don't use animations in this suite. + time = 0; store = createStore(cameraReducer, undefined); translationShouldBeCloseTo = expectedTranslation => { - const actualTranslation = translation(store.getState()); + const actualTranslation = translation(store.getState())(time); expect(expectedTranslation[0]).toBeCloseTo(actualTranslation[0]); expect(expectedTranslation[1]).toBeCloseTo(actualTranslation[1]); }; @@ -30,94 +33,64 @@ describe('panning interaction', () => { it('should have a translation of 0,0', () => { translationShouldBeCloseTo([0, 0]); }); - describe('when the user has started panning', () => { + describe('when the user has started panning at (100, 100)', () => { beforeEach(() => { - const action: CameraAction = { type: 'userStartedPanning', payload: [100, 100] }; + const action: CameraAction = { + type: 'userStartedPanning', + payload: { screenCoordinates: [100, 100], time }, + }; store.dispatch(action); }); it('should have a translation of 0,0', () => { translationShouldBeCloseTo([0, 0]); }); - describe('when the user continues to pan 50px up and to the right', () => { + describe('when the user moves their pointer 50px up and right (towards the top right of the screen)', () => { beforeEach(() => { - const action: CameraAction = { type: 'userMovedPointer', payload: [150, 50] }; + const action: CameraAction = { + type: 'userMovedPointer', + payload: { screenCoordinates: [150, 50], time }, + }; store.dispatch(action); }); - it('should have a translation of 50,50', () => { - translationShouldBeCloseTo([50, 50]); + it('should have a translation of [-50, -50] as the camera is now focused on things lower and to the left.', () => { + translationShouldBeCloseTo([-50, -50]); }); describe('when the user then stops panning', () => { beforeEach(() => { - const action: CameraAction = { type: 'userStoppedPanning' }; + const action: CameraAction = { + type: 'userStoppedPanning', + payload: { time }, + }; store.dispatch(action); }); - it('should have a translation of 50,50', () => { - translationShouldBeCloseTo([50, 50]); + it('should still have a translation of [-50, -50]', () => { + translationShouldBeCloseTo([-50, -50]); }); }); }); }); }); - describe('panning controls', () => { - describe('when user clicks on pan north button', () => { - beforeEach(() => { - const action: CameraAction = { type: 'userClickedPanControl', payload: 'north' }; - store.dispatch(action); - }); - it('moves the camera south so that objects appear closer to the bottom of the screen', () => { - const actual = translation(store.getState()); - expect(actual).toMatchInlineSnapshot(` - Array [ - 0, - -32.49906769231164, - ] - `); - }); - }); - describe('when user clicks on pan south button', () => { - beforeEach(() => { - const action: CameraAction = { type: 'userClickedPanControl', payload: 'south' }; - store.dispatch(action); - }); - it('moves the camera north so that objects appear closer to the top of the screen', () => { - const actual = translation(store.getState()); - expect(actual).toMatchInlineSnapshot(` - Array [ - 0, - 32.49906769231164, - ] - `); - }); - }); - describe('when user clicks on pan east button', () => { - beforeEach(() => { - const action: CameraAction = { type: 'userClickedPanControl', payload: 'east' }; - store.dispatch(action); - }); - it('moves the camera west so that objects appear closer to the left of the screen', () => { - const actual = translation(store.getState()); - expect(actual).toMatchInlineSnapshot(` - Array [ - -32.49906769231164, - 0, - ] - `); - }); + describe('when the user nudges the camera up', () => { + beforeEach(() => { + const action: CameraAction = { + type: 'userNudgedCamera', + payload: { direction: [0, 1], time }, + }; + store.dispatch(action); }); - describe('when user clicks on pan west button', () => { - beforeEach(() => { - const action: CameraAction = { type: 'userClickedPanControl', payload: 'west' }; - store.dispatch(action); - }); - it('moves the camera east so that objects appear closer to the right of the screen', () => { - const actual = translation(store.getState()); - expect(actual).toMatchInlineSnapshot(` - Array [ - 32.49906769231164, - 0, - ] - `); - }); + it('the camera eventually moves up so that objects appear closer to the bottom of the screen', () => { + const aBitIntoTheFuture = time + 100; + + /** + * Check the position once the animation has advanced 100ms + */ + const actual: Vector2 = translation(store.getState())(aBitIntoTheFuture); + expect(actual).toMatchInlineSnapshot(` + Array [ + 0, + 7.4074074074074066, + ] + `); }); }); }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts index e21e3d10017949..e868424d06c94d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts @@ -18,11 +18,21 @@ describe('projectionMatrix', () => { beforeEach(() => { store = createStore(cameraReducer, undefined); compare = (worldPosition: [number, number], expectedRasterPosition: [number, number]) => { - const [rasterX, rasterY] = applyMatrix3(worldPosition, projectionMatrix(store.getState())); + // time isn't really relevant as we aren't testing animation + const time = 0; + const [rasterX, rasterY] = applyMatrix3( + worldPosition, + projectionMatrix(store.getState())(time) + ); expect(rasterX).toBeCloseTo(expectedRasterPosition[0]); expect(rasterY).toBeCloseTo(expectedRasterPosition[1]); }; }); + describe('when the raster size is 0 x 0 pixels (unpainted)', () => { + it('should convert 0,0 (center) in world space to 0,0 in raster space', () => { + compare([0, 0], [0, 0]); + }); + }); describe('when the raster size is 300 x 200 pixels', () => { beforeEach(() => { const action: CameraAction = { type: 'userSetRasterSize', payload: [300, 200] }; @@ -66,7 +76,7 @@ describe('projectionMatrix', () => { }); describe('when the user has panned to the right and up by 50', () => { beforeEach(() => { - const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-50, -50] }; + const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [50, 50] }; store.dispatch(action); }); it('should convert 0,0 (center) in world space to 100,150 in raster space', () => { @@ -83,7 +93,7 @@ describe('projectionMatrix', () => { beforeEach(() => { const action: CameraAction = { type: 'userSetPositionOfCamera', - payload: [-350, -250], + payload: [350, 250], }; store.dispatch(action); }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts index 7c4678a4f1dc13..0f6ae1b7d904a8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts @@ -5,24 +5,32 @@ */ import { Reducer } from 'redux'; -import { applyMatrix3, subtract } from '../../lib/vector2'; -import { userIsPanning, translation, projectionMatrix, inverseProjectionMatrix } from './selectors'; +import { unitsPerNudge, nudgeAnimationDuration } from './scaling_constants'; +import { animatePanning } from './methods'; +import * as vector2 from '../../lib/vector2'; +import * as selectors from './selectors'; import { clamp } from '../../lib/math'; import { CameraState, ResolverAction, Vector2 } from '../../types'; import { scaleToZoom } from './scale_to_zoom'; -function initialState(): CameraState { - return { +/** + * Used in tests. + */ +export function cameraInitialState(): CameraState { + const state: CameraState = { scalingFactor: scaleToZoom(1), // Defaulted to 1 to 1 scale rasterSize: [0, 0] as const, translationNotCountingCurrentPanning: [0, 0] as const, latestFocusedWorldCoordinates: null, + animation: undefined, + panning: undefined, }; + return state; } export const cameraReducer: Reducer<CameraState, ResolverAction> = ( - state = initialState(), + state = cameraInitialState(), action ) => { if (action.type === 'userSetZoomLevel') { @@ -30,10 +38,11 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = ( * Handle the scale being explicitly set, for example by a 'reset zoom' feature, or by a range slider with exact scale values */ - return { + const nextState: CameraState = { ...state, scalingFactor: clamp(action.payload, 0, 1), }; + return nextState; } else if (action.type === 'userClickedZoomIn') { return { ...state, @@ -47,7 +56,7 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = ( } else if (action.type === 'userZoomed') { const stateWithNewScaling: CameraState = { ...state, - scalingFactor: clamp(state.scalingFactor + action.payload, 0, 1), + scalingFactor: clamp(state.scalingFactor + action.payload.zoomChange, 0, 1), }; /** @@ -59,22 +68,41 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = ( * using CTRL and the mousewheel, or by pinching the trackpad on a Mac. The node will stay under your mouse cursor and other things in the map will get * nearer or further from the mouse cursor. This lets you keep your context when changing zoom levels. */ - if (state.latestFocusedWorldCoordinates !== null) { - const rasterOfLastFocusedWorldCoordinates = applyMatrix3( + if ( + state.latestFocusedWorldCoordinates !== null && + !selectors.isAnimating(state)(action.payload.time) + ) { + const rasterOfLastFocusedWorldCoordinates = vector2.applyMatrix3( state.latestFocusedWorldCoordinates, - projectionMatrix(state) + selectors.projectionMatrix(state)(action.payload.time) + ); + const newWorldCoordinatesAtLastFocusedPosition = vector2.applyMatrix3( + rasterOfLastFocusedWorldCoordinates, + selectors.inverseProjectionMatrix(stateWithNewScaling)(action.payload.time) + ); + + /** + * The change in world position incurred by changing scale. + */ + const delta = vector2.subtract( + newWorldCoordinatesAtLastFocusedPosition, + state.latestFocusedWorldCoordinates ); - const matrix = inverseProjectionMatrix(stateWithNewScaling); - const worldCoordinateThereNow = applyMatrix3(rasterOfLastFocusedWorldCoordinates, matrix); - const delta = subtract(worldCoordinateThereNow, state.latestFocusedWorldCoordinates); - return { + /** + * Adjust for the change in position due to scale. + */ + const translationNotCountingCurrentPanning: Vector2 = vector2.subtract( + stateWithNewScaling.translationNotCountingCurrentPanning, + delta + ); + + const nextState: CameraState = { ...stateWithNewScaling, - translationNotCountingCurrentPanning: [ - stateWithNewScaling.translationNotCountingCurrentPanning[0] + delta[0], - stateWithNewScaling.translationNotCountingCurrentPanning[1] + delta[1], - ], + translationNotCountingCurrentPanning, }; + + return nextState; } else { return stateWithNewScaling; } @@ -82,83 +110,76 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = ( /** * Handle the case where the position of the camera is explicitly set, for example by a 'back to center' feature. */ - return { + const nextState: CameraState = { ...state, + animation: undefined, translationNotCountingCurrentPanning: action.payload, }; + return nextState; } else if (action.type === 'userStartedPanning') { + if (selectors.isAnimating(state)(action.payload.time)) { + return state; + } /** * When the user begins panning with a mousedown event we mark the starting position for later comparisons. */ - return { + const nextState: CameraState = { ...state, + animation: undefined, panning: { - origin: action.payload, - currentOffset: action.payload, + origin: action.payload.screenCoordinates, + currentOffset: action.payload.screenCoordinates, }, }; + return nextState; } else if (action.type === 'userStoppedPanning') { /** * When the user stops panning (by letting up on the mouse) we calculate the new translation of the camera. */ - if (userIsPanning(state)) { - return { - ...state, - translationNotCountingCurrentPanning: translation(state), - panning: undefined, - }; - } else { - return state; - } - } else if (action.type === 'userClickedPanControl') { - const panDirection = action.payload; + const nextState: CameraState = { + ...state, + translationNotCountingCurrentPanning: selectors.translation(state)(action.payload.time), + panning: undefined, + }; + return nextState; + } else if (action.type === 'userNudgedCamera') { + const { direction, time } = action.payload; /** - * Delta amount will be in the range of 20 -> 40 depending on the scalingFactor + * Nudge less when zoomed in. */ - const deltaAmount = (1 + state.scalingFactor) * 20; - let delta: Vector2; - if (panDirection === 'north') { - delta = [0, -deltaAmount]; - } else if (panDirection === 'south') { - delta = [0, deltaAmount]; - } else if (panDirection === 'east') { - delta = [-deltaAmount, 0]; - } else if (panDirection === 'west') { - delta = [deltaAmount, 0]; - } else { - delta = [0, 0]; - } + const nudge = vector2.multiply( + vector2.divide([unitsPerNudge, unitsPerNudge], selectors.scale(state)(time)), + direction + ); - return { - ...state, - translationNotCountingCurrentPanning: [ - state.translationNotCountingCurrentPanning[0] + delta[0], - state.translationNotCountingCurrentPanning[1] + delta[1], - ], - }; + return animatePanning( + state, + time, + vector2.add(state.translationNotCountingCurrentPanning, nudge), + nudgeAnimationDuration + ); } else if (action.type === 'userSetRasterSize') { /** * Handle resizes of the Resolver component. We need to know the size in order to convert between screen * and world coordinates. */ - return { + const nextState: CameraState = { ...state, rasterSize: action.payload, }; + return nextState; } else if (action.type === 'userMovedPointer') { - const stateWithUpdatedPanning = { - ...state, - /** - * If the user is panning, adjust the panning offset - */ - panning: userIsPanning(state) - ? { - origin: state.panning ? state.panning.origin : action.payload, - currentOffset: action.payload, - } - : state.panning, - }; - return { + let stateWithUpdatedPanning: CameraState = state; + if (state.panning) { + stateWithUpdatedPanning = { + ...state, + panning: { + origin: state.panning.origin, + currentOffset: action.payload.screenCoordinates, + }, + }; + } + const nextState: CameraState = { ...stateWithUpdatedPanning, /** * keep track of the last world coordinates the user moved over. @@ -166,11 +187,12 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = ( * to keep the same point under the pointer. * In order to do this, we need to know the position of the mouse when changing the scale. */ - latestFocusedWorldCoordinates: applyMatrix3( - action.payload, - inverseProjectionMatrix(stateWithUpdatedPanning) + latestFocusedWorldCoordinates: vector2.applyMatrix3( + action.payload.screenCoordinates, + selectors.inverseProjectionMatrix(stateWithUpdatedPanning)(action.payload.time) ), }; + return nextState; } else { return state; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/scaling_constants.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/scaling_constants.ts index 93c41fde64f0ea..243d8877a8b0d7 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/scaling_constants.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/scaling_constants.ts @@ -7,7 +7,7 @@ /** * The minimum allowed value for the camera scale. This is the least scale that we will ever render something at. */ -export const minimum = 0.1; +export const minimum = 0.5; /** * The maximum allowed value for the camera scale. This is greatest scale that we will ever render something at. @@ -18,3 +18,13 @@ export const maximum = 6; * The curve of the zoom function growth rate. The higher the scale factor is, the higher the zoom rate will be. */ export const zoomCurveRate = 4; + +/** + * The size, in world units, of a 'nudge' as caused by clicking the up, right, down, or left panning buttons. + */ +export const unitsPerNudge = 50; + +/** + * The duration a nudge animation lasts. + */ +export const nudgeAnimationDuration = 300; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index 53ffe6dd073fa6..226e36f63d788e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Vector2, CameraState, AABB, Matrix3 } from '../../types'; -import { subtract, divide, add, applyMatrix3 } from '../../lib/vector2'; +import { createSelector, defaultMemoize } from 'reselect'; +import { easing } from 'ts-easing'; +import { clamp, lerp } from '../../lib/math'; +import * as vector2 from '../../lib/vector2'; import { multiply, add as addMatrix } from '../../lib/matrix3'; import { inverseOrthographicProjection, @@ -13,7 +15,8 @@ import { orthographicProjection, translationTransformation, } from '../../lib/transformation'; -import { maximum, minimum, zoomCurveRate } from './scaling_constants'; +import * as scalingConstants from './scaling_constants'; +import { Vector2, CameraState, AABB, Matrix3, CameraAnimationState } from '../../types'; interface ClippingPlanes { renderWidth: number; @@ -24,77 +27,283 @@ interface ClippingPlanes { clippingPlaneBottom: number; } +function animationIsActive(animation: CameraAnimationState, time: number): boolean { + return animation.startTime + animation.duration >= time; +} + /** - * The viewable area in the Resolver map, in world coordinates. + * The scale by which world values are scaled when rendered. + * + * When the camera position (translation) is changed programatically, it may be animated. + * The duration of the animation is generally fixed for a given type of interaction. This way + * the user won't have to wait for a variable amount of time to complete their interaction. + * + * Since the duration is fixed and the amount that the camera position changes is variable, + * the speed at which the camera changes is also variable. If the distance the camera will move + * is very far, the camera will move very fast. + * + * When the camera moves fast, elements will move across the screen quickly. These + * quick moving elements can be distracting to the user. They may also hinder the quality of + * animation. + * + * The speed at which objects move across the screen is dependent on the speed of the camera + * as well as the scale. If the scale is high, the camera is zoomed in, and so objects move + * across the screen faster at a given camera speed. Think of looking into a telephoto lense + * and moving around only a few degrees: many things might pass through your sight. + * + * If the scale is low, the camera is zoomed out, objects look further away, and so they move + * across the screen slower at a given camera speed. Therefore we can control the speed at + * which objects move across the screen without changing the camera speed. We do this by changing scale. + * + * Changing the scale abruptly isn't acceptable because it would be visually jarring. Also, the + * change in scale should be temporary, and the original scale should be resumed after the animation. + * + * In order to change the scale to lower value, and then back, without being jarring to the user, + * we calculate a temporary target scale and animate to it. + * */ -export function viewableBoundingBox(state: CameraState): AABB { - const { renderWidth, renderHeight } = clippingPlanes(state); - const matrix = inverseProjectionMatrix(state); - const bottomLeftCorner: Vector2 = [0, renderHeight]; - const topRightCorner: Vector2 = [renderWidth, 0]; - return { - minimum: applyMatrix3(bottomLeftCorner, matrix), - maximum: applyMatrix3(topRightCorner, matrix), - }; -} +export const scale: (state: CameraState) => (time: number) => Vector2 = createSelector( + state => state.scalingFactor, + state => state.animation, + (scalingFactor, animation) => { + const scaleNotCountingAnimation = scaleFromScalingFactor(scalingFactor); + /** + * If `animation` is defined, an animation may be in progress when the returned function is called + */ + if (animation !== undefined) { + /** + * The distance the camera will move during the animation is used to determine the camera speed. + */ + const panningDistance = vector2.distance( + animation.targetTranslation, + animation.initialTranslation + ); + + const panningDistanceInPixels = panningDistance * scaleNotCountingAnimation; + + /** + * The speed at which pixels move across the screen during animation in pixels per millisecond. + */ + const speed = panningDistanceInPixels / animation.duration; + + /** + * The speed (in pixels per millisecond) at which an animation is triggered is a constant. + * If the camera isn't moving very fast, no change in scale is necessary. + */ + const speedThreshold = 0.4; + + /** + * Growth in speed beyond the threshold is taken to the power of a constant. This limits the + * rate of growth of speed. + */ + const speedGrowthFactor = 0.4; + + /* + * Limit the rate of growth of speed. If the speed is too great, the animation will be + * unpleasant and have poor performance. + * + * gnuplot> plot [x=0:10][y=0:3] threshold=0.4, growthFactor=0.4, x < threshold ? x : x ** growthFactor - (threshold ** growthFactor - threshold) + * + * + * 3 +----------------------------------------------------------------------------+ + * | target speed + + + | + * | | + * | ******* | + * | | + * | | + * 2.5 |-+ +-| + * | | + * | | + * | **| + * | ******* | + * | ****** | + * 2 |-+ ****** +-| + * | ***** | + * | ***** | + * | ***** | + * | ***** | + * 1.5 |-+ ***** +-| + * | **** | + * | **** | + * | **** | + * | *** | + * | *** | + * 1 |-+ ** +-| + * | *** | + * | *** | + * | * | + * | ** | + * | ** | + * 0.5 |-+ * +-| + * | ** | + * | * | + * | * | + * | * | + * |* + + + + | + * 0 +----------------------------------------------------------------------------+ + * 0 2 4 6 8 10 + * camera speed (pixels per ms) + * + **/ + const limitedSpeed = + speed < speedThreshold + ? speed + : speed ** speedGrowthFactor - (speedThreshold ** speedGrowthFactor - speedThreshold); + + /** + * The distance and duration of the animation are independent variables. If the speed was + * limited, only the scale can change. The lower the scale, the further the camera is + * away from things, and therefore the slower things move across the screen. Adjust the + * scale (within its own limits) to match the limited speed. + * + * This will cause the camera to zoom out if it would otherwise move too fast. + */ + const adjustedScale = clamp( + (limitedSpeed * animation.duration) / panningDistance, + scalingConstants.minimum, + scalingConstants.maximum + ); + + return time => { + /** + * If the animation has completed, return the `scaleNotCountingAnimation`, as + * the animation always completes with the scale set back at starting value. + */ + if (animationIsActive(animation, time) === false) { + return [scaleNotCountingAnimation, scaleNotCountingAnimation]; + } else { + /** + * + * Animation is defined by a starting time, duration, starting position, and ending position. The amount of time + * which has passed since the start time, compared to the duration, defines the progress of the animation. + * We represent this process with a number between 0 and 1. As the animation progresses, the value changes from 0 + * to 1, linearly. + */ + const x = animationProgress(animation, time); + /** + * The change in scale over the duration of the animation should not be linear. It should grow to the target value, + * then shrink back down to the original value. We adjust the animation progress so that it reaches its peak + * halfway through the animation and then returns to the beginning value by the end of the animation. + * + * We ease the value so that the change from not-animating-at-all to animating-at-full-speed isn't abrupt. + * See the graph: + * + * gnuplot> plot [x=-0:1][x=0:1.2] eased(t)=t<.5? 4*t**3 : (t-1)*(2*t-2)**2+1, progress(t)=-abs(2*t-1)+1, eased(progress(x)) + * + * + * 1.2 +--------------------------------------------------------------------------------------+ + * | + + + + | + * | e(t)=t<.5? 4*t**3 : (t-1)*(2*t-2)**2+1, t(x)=-abs(2*x-1)+1, e(t(x)) ******* | + * | | + * | | + * | | + * 1 |-+ **************** +-| + * | *** *** | + * | ** ** | + * | ** ** | + * | * * | + * | * * | + * 0.8 |-+ * * +-| + * | * * | + * | * * | + * | * * | + * | * * | + * 0.6 |-+ * * +-| + * | * * | + * | * * | + * | * * | + * | * * | + * | * * | + * 0.4 |-+ * * +-| + * | * * | + * | * * | + * | * * | + * | * * | + * | * * | + * 0.2 |-+ * * +-| + * | * * | + * | * * | + * | ** ** | + * | * * | + * | *** + + + + *** | + * 0 +--------------------------------------------------------------------------------------+ + * 0 0.2 0.4 0.6 0.8 1 + * animation progress + * + */ + const easedInOutAnimationProgress = easing.inOutCubic(-Math.abs(2 * x - 1) + 1); + + /** + * Linearly interpolate between these, using the bell-shaped easing value + */ + const lerpedScale = lerp( + scaleNotCountingAnimation, + adjustedScale, + easedInOutAnimationProgress + ); + + /** + * The scale should be the same in both axes. + */ + return [lerpedScale, lerpedScale]; + } + }; + } else { + /** + * The scale should be the same in both axes. + */ + return () => [scaleNotCountingAnimation, scaleNotCountingAnimation]; + } + + /** + * Interpolate between the minimum and maximum scale, + * using a curved ratio based on `factor`. + */ + function scaleFromScalingFactor(factor: number): number { + return lerp( + scalingConstants.minimum, + scalingConstants.maximum, + Math.pow(factor, scalingConstants.zoomCurveRate) + ); + } + } +); /** * The 2D clipping planes used for the orthographic projection. See https://en.wikipedia.org/wiki/Orthographic_projection */ -function clippingPlanes(state: CameraState): ClippingPlanes { - const renderWidth = state.rasterSize[0]; - const renderHeight = state.rasterSize[1]; - const clippingPlaneRight = renderWidth / 2 / scale(state)[0]; - const clippingPlaneTop = renderHeight / 2 / scale(state)[1]; - - return { - renderWidth, - renderHeight, - clippingPlaneRight, - clippingPlaneTop, - clippingPlaneLeft: -clippingPlaneRight, - clippingPlaneBottom: -clippingPlaneTop, - }; -} +export const clippingPlanes: ( + state: CameraState +) => (time: number) => ClippingPlanes = createSelector( + state => state.rasterSize, + scale, + (rasterSize, scaleAtTime) => (time: number) => { + const [scaleX, scaleY] = scaleAtTime(time); + const renderWidth = rasterSize[0]; + const renderHeight = rasterSize[1]; + const clippingPlaneRight = renderWidth / 2 / scaleX; + const clippingPlaneTop = renderHeight / 2 / scaleY; + + return { + renderWidth, + renderHeight, + clippingPlaneRight, + clippingPlaneTop, + clippingPlaneLeft: -clippingPlaneRight, + clippingPlaneBottom: -clippingPlaneTop, + }; + } +); /** - * A matrix that when applied to a Vector2 will convert it from world coordinates to screen coordinates. - * See https://en.wikipedia.org/wiki/Orthographic_projection + * Whether or not the camera is animating, at a given time. */ -export const projectionMatrix: (state: CameraState) => Matrix3 = state => { - const { - renderWidth, - renderHeight, - clippingPlaneRight, - clippingPlaneTop, - clippingPlaneLeft, - clippingPlaneBottom, - } = clippingPlanes(state); - - return multiply( - // 5. convert from 0->2 to 0->rasterWidth (or height) - scalingTransformation([renderWidth / 2, renderHeight / 2]), - addMatrix( - // 4. add one to change range from -1->1 to 0->2 - [0, 0, 1, 0, 0, 1, 0, 0, 0], - multiply( - // 3. invert y since CSS has inverted y - scalingTransformation([1, -1]), - multiply( - // 2. scale to clipping plane - orthographicProjection( - clippingPlaneTop, - clippingPlaneRight, - clippingPlaneBottom, - clippingPlaneLeft - ), - // 1. adjust for camera - translationTransformation(translation(state)) - ) - ) - ) - ); -}; +export const isAnimating: (state: CameraState) => (time: number) => boolean = createSelector( + state => state.animation, + animation => time => { + return animation !== undefined && animationIsActive(animation, time); + } +); /** * The camera has a translation value (not counting any current panning.) This is initialized to (0, 0) and @@ -108,79 +317,186 @@ export const projectionMatrix: (state: CameraState) => Matrix3 = state => { * * We could update the translation as the user moved the mouse but floating point drift (round-off error) could occur. */ -export function translation(state: CameraState): Vector2 { - if (state.panning) { - return add( - state.translationNotCountingCurrentPanning, - divide(subtract(state.panning.currentOffset, state.panning.origin), [ - scale(state)[0], - // Invert `y` since the `.panning` vectors are in screen coordinates and therefore have backwards `y` - -scale(state)[1], - ]) - ); - } else { - return state.translationNotCountingCurrentPanning; +export const translation: (state: CameraState) => (time: number) => Vector2 = createSelector( + state => state.panning, + state => state.translationNotCountingCurrentPanning, + scale, + state => state.animation, + (panning, translationNotCountingCurrentPanning, scaleAtTime, animation) => { + return (time: number) => { + const [scaleX, scaleY] = scaleAtTime(time); + if (animation !== undefined && animationIsActive(animation, time)) { + return vector2.lerp( + animation.initialTranslation, + animation.targetTranslation, + easing.inOutCubic(animationProgress(animation, time)) + ); + } else if (panning) { + const changeInPanningOffset = vector2.subtract(panning.currentOffset, panning.origin); + /** + * invert the vector since panning moves the perception of the screen, which is inverse of the + * translation of the camera. Inverse the `y` axis again, since `y` is inverted between + * world and screen coordinates. + */ + const changeInTranslation = vector2.divide(changeInPanningOffset, [-scaleX, scaleY]); + return vector2.add(translationNotCountingCurrentPanning, changeInTranslation); + } else { + return translationNotCountingCurrentPanning; + } + }; } -} +); /** * A matrix that when applied to a Vector2 converts it from screen coordinates to world coordinates. * See https://en.wikipedia.org/wiki/Orthographic_projection */ -export const inverseProjectionMatrix: (state: CameraState) => Matrix3 = state => { - const { - renderWidth, - renderHeight, - clippingPlaneRight, - clippingPlaneTop, - clippingPlaneLeft, - clippingPlaneBottom, - } = clippingPlanes(state); - - /* prettier-ignore */ - const screenToNDC = [ - 2 / renderWidth, 0, -1, - 0, 2 / renderHeight, -1, - 0, 0, 0 - ] as const - - const [translationX, translationY] = translation(state); - - return addMatrix( - // 4. Translate for the 'camera' - // prettier-ignore - [ - 0, 0, -translationX, - 0, 0, -translationY, - 0, 0, 0 - ] as const, - multiply( - // 3. make values in range of clipping planes - inverseOrthographicProjection( +export const inverseProjectionMatrix: ( + state: CameraState +) => (time: number) => Matrix3 = createSelector( + clippingPlanes, + translation, + (clippingPlanesAtTime, translationAtTime) => { + return (time: number) => { + const { + renderWidth, + renderHeight, + clippingPlaneRight, + clippingPlaneTop, + clippingPlaneLeft, + clippingPlaneBottom, + } = clippingPlanesAtTime(time); + + /** + * 1. Convert from 0<=n<=screenDimension to -1<=n<=1 + * e.g. for x-axis, divide by renderWidth then multiply by 2 and subtract by one so the value is in range of -1->1 + */ + // prettier-ignore + const screenToNDC: Matrix3 = [ + renderWidth === 0 ? 0 : 2 / renderWidth, 0, -1, + 0, renderHeight === 0 ? 0 : 2 / renderHeight, -1, + 0, 0, 0 + ]; + + /** + * 2. Invert Y since DOM positioning has inverted Y axis + */ + const invertY = scalingTransformation([1, -1]); + + const [translationX, translationY] = translationAtTime(time); + + /** + * 3. Scale values to the clipping plane dimensions. + */ + const scaleToClippingPlaneDimensions = inverseOrthographicProjection( clippingPlaneTop, clippingPlaneRight, clippingPlaneBottom, clippingPlaneLeft - ), - multiply( - // 2 Invert Y since CSS has inverted y - scalingTransformation([1, -1]), - // 1. convert screen coordinates to NDC - // e.g. for x-axis, divide by renderWidth then multiply by 2 and subtract by one so the value is in range of -1->1 - screenToNDC - ) - ) - ); -}; + ); + + /** + * Move the values to accomodate for the perspective of the camera (based on the camera's transform) + */ + const translateForCamera: Matrix3 = [0, 0, translationX, 0, 0, translationY, 0, 0, 0]; + + return addMatrix( + translateForCamera, + multiply(scaleToClippingPlaneDimensions, multiply(invertY, screenToNDC)) + ); + }; + } +); /** - * The scale by which world values are scaled when rendered. + * The viewable area in the Resolver map, in world coordinates. */ -export const scale = (state: CameraState): Vector2 => { - const delta = maximum - minimum; - const value = Math.pow(state.scalingFactor, zoomCurveRate) * delta + minimum; - return [value, value]; -}; +export const viewableBoundingBox: (state: CameraState) => (time: number) => AABB = createSelector( + clippingPlanes, + inverseProjectionMatrix, + (clippingPlanesAtTime, matrixAtTime) => { + return (time: number) => { + const { renderWidth, renderHeight } = clippingPlanesAtTime(time); + const matrix = matrixAtTime(time); + const bottomLeftCorner: Vector2 = [0, renderHeight]; + const topRightCorner: Vector2 = [renderWidth, 0]; + return { + minimum: vector2.applyMatrix3(bottomLeftCorner, matrix), + maximum: vector2.applyMatrix3(topRightCorner, matrix), + }; + }; + } +); + +/** + * A matrix that when applied to a Vector2 will convert it from world coordinates to screen coordinates. + * See https://en.wikipedia.org/wiki/Orthographic_projection + */ +export const projectionMatrix: (state: CameraState) => (time: number) => Matrix3 = createSelector( + clippingPlanes, + translation, + (clippingPlanesAtTime, translationAtTime) => { + return defaultMemoize((time: number) => { + const { + renderWidth, + renderHeight, + clippingPlaneRight, + clippingPlaneTop, + clippingPlaneLeft, + clippingPlaneBottom, + } = clippingPlanesAtTime(time); + + /** + * 1. adjust for camera by subtracting its translation. The closer the camera is to a point, the closer that point + * should be to the center of the screen. + */ + const adjustForCameraPosition = translationTransformation( + vector2.scale(translationAtTime(time), -1) + ); + + /** + * 2. Scale the values based on the dimsension of Resolver on the screen. + */ + const screenToNDC = orthographicProjection( + clippingPlaneTop, + clippingPlaneRight, + clippingPlaneBottom, + clippingPlaneLeft + ); + + /** + * 3. invert y since CSS has inverted y + */ + const invertY = scalingTransformation([1, -1]); + + /** + * 3. Convert values from the scale of -1<=n<=1 to 0<=n<=2 + */ + // prettier-ignore + const fromNDCtoZeroToTwo: Matrix3 = [ + 0, 0, 1, + 0, 0, 1, + 0, 0, 0 + ] + + /** + * 4. convert from 0->2 to 0->rasterDimension by multiplying by rasterDimension/2 + */ + const fromZeroToTwoToRasterDimensions = scalingTransformation([ + renderWidth / 2, + renderHeight / 2, + ]); + + return multiply( + fromZeroToTwoToRasterDimensions, + addMatrix( + fromNDCtoZeroToTwo, + multiply(invertY, multiply(screenToNDC, adjustForCameraPosition)) + ) + ); + }); + } +); /** * Scales the coordinate system, used for zooming. Should always be between 0 and 1 @@ -193,3 +509,12 @@ export const scalingFactor = (state: CameraState): CameraState['scalingFactor'] * Whether or not the user is current panning the map. */ export const userIsPanning = (state: CameraState): boolean => state.panning !== undefined; + +/** + * Returns a number 0<=n<=1 where: + * 0 meaning it just started, + * 1 meaning it is done. + */ +function animationProgress(animation: CameraAnimationState, time: number): number { + return clamp((time - animation.startTime) / animation.duration, 0, 1); +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts index abc113d5999ffe..fb38c2f526e0b3 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts @@ -15,12 +15,13 @@ import { applyMatrix3 } from '../../lib/vector2'; describe('zooming', () => { let store: Store<CameraState, CameraAction>; + let time: number; const cameraShouldBeBoundBy = (expectedViewableBoundingBox: AABB): [string, () => void] => { return [ `the camera view should be bound by an AABB with a minimum point of ${expectedViewableBoundingBox.minimum} and a maximum point of ${expectedViewableBoundingBox.maximum}`, () => { - const actual = viewableBoundingBox(store.getState()); + const actual = viewableBoundingBox(store.getState())(time); expect(actual.minimum[0]).toBeCloseTo(expectedViewableBoundingBox.minimum[0]); expect(actual.minimum[1]).toBeCloseTo(expectedViewableBoundingBox.minimum[1]); expect(actual.maximum[0]).toBeCloseTo(expectedViewableBoundingBox.maximum[0]); @@ -29,6 +30,8 @@ describe('zooming', () => { ]; }; beforeEach(() => { + // Time isn't relevant as we aren't testing animation + time = 0; store = createStore(cameraReducer, undefined); }); describe('when the raster size is 300 x 200 pixels', () => { @@ -58,12 +61,12 @@ describe('zooming', () => { beforeEach(() => { const action: CameraAction = { type: 'userZoomed', - payload: 1, + payload: { zoomChange: 1, time }, }; store.dispatch(action); }); it('should zoom to maximum scale factor', () => { - const actual = viewableBoundingBox(store.getState()); + const actual = viewableBoundingBox(store.getState())(time); expect(actual).toMatchInlineSnapshot(` Object { "maximum": Array [ @@ -79,16 +82,16 @@ describe('zooming', () => { }); }); it('the raster position 200, 50 should map to the world position 50, 50', () => { - expectVectorsToBeClose(applyMatrix3([200, 50], inverseProjectionMatrix(store.getState())), [ - 50, - 50, - ]); + expectVectorsToBeClose( + applyMatrix3([200, 50], inverseProjectionMatrix(store.getState())(time)), + [50, 50] + ); }); describe('when the user has moved their mouse to the raster position 200, 50', () => { beforeEach(() => { const action: CameraAction = { type: 'userMovedPointer', - payload: [200, 50], + payload: { screenCoordinates: [200, 50], time }, }; store.dispatch(action); }); @@ -104,13 +107,13 @@ describe('zooming', () => { beforeEach(() => { const action: CameraAction = { type: 'userZoomed', - payload: 0.5, + payload: { zoomChange: 0.5, time }, }; store.dispatch(action); }); it('the raster position 200, 50 should map to the world position 50, 50', () => { expectVectorsToBeClose( - applyMatrix3([200, 50], inverseProjectionMatrix(store.getState())), + applyMatrix3([200, 50], inverseProjectionMatrix(store.getState())(time)), [50, 50] ); }); @@ -118,7 +121,7 @@ describe('zooming', () => { }); describe('when the user pans right by 100 pixels', () => { beforeEach(() => { - const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-100, 0] }; + const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [100, 0] }; store.dispatch(action); }); it( @@ -130,7 +133,7 @@ describe('zooming', () => { it('should be centered on 100, 0', () => { const worldCenterPoint = applyMatrix3( [150, 100], - inverseProjectionMatrix(store.getState()) + inverseProjectionMatrix(store.getState())(time) ); expect(worldCenterPoint[0]).toBeCloseTo(100); expect(worldCenterPoint[1]).toBeCloseTo(0); @@ -143,7 +146,7 @@ describe('zooming', () => { it('should be centered on 100, 0', () => { const worldCenterPoint = applyMatrix3( [150, 100], - inverseProjectionMatrix(store.getState()) + inverseProjectionMatrix(store.getState())(time) ); expect(worldCenterPoint[0]).toBeCloseTo(100); expect(worldCenterPoint[1]).toBeCloseTo(0); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap index 261ca7e0a7bbaf..1dc17054b9f47c 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap @@ -18,6 +18,7 @@ Object { "node_id": 0, "process_name": "", "process_path": "", + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -172,6 +173,7 @@ Object { "node_id": 0, "process_name": "", "process_path": "", + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -188,6 +190,7 @@ Object { "process_name": "", "process_path": "", "source_id": 0, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -204,6 +207,7 @@ Object { "process_name": "", "process_path": "", "source_id": 0, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -220,6 +224,7 @@ Object { "process_name": "", "process_path": "", "source_id": 1, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -236,6 +241,7 @@ Object { "process_name": "", "process_path": "", "source_id": 1, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -252,6 +258,7 @@ Object { "process_name": "", "process_path": "", "source_id": 2, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -268,6 +275,7 @@ Object { "process_name": "", "process_path": "", "source_id": 2, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -284,6 +292,7 @@ Object { "process_name": "", "process_path": "", "source_id": 6, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -318,6 +327,7 @@ Object { "node_id": 0, "process_name": "", "process_path": "", + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -334,6 +344,7 @@ Object { "process_name": "", "process_path": "", "source_id": 0, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 745bd125c151d2..75b477dd7c7fcd 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -57,11 +57,17 @@ const isometricTransformMatrix: Matrix3 = [ /** * The distance in pixels (at scale 1) between nodes. Change this to space out nodes more */ -export const distanceBetweenNodes = distanceBetweenNodesInUnits * unit; +const distanceBetweenNodes = distanceBetweenNodesInUnits * unit; -export function graphableProcesses(state: DataState) { - return state.results.filter(isGraphableProcess); -} +/** + * Process events that will be graphed. + */ +export const graphableProcesses = createSelector( + ({ results }: DataState) => results, + function(results: DataState['results']) { + return results.filter(isGraphableProcess); + } +); /** * In laying out the graph, we precalculate the 'width' of each subtree. The 'width' of the subtree is determined by its diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts index d043453a8e4cdf..b17572bbc4ab46 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts @@ -4,43 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createStore, StoreEnhancer } from 'redux'; -import { ResolverAction } from '../types'; +import { createStore, applyMiddleware, Store } from 'redux'; +import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly'; +import { ResolverAction, ResolverState } from '../types'; import { resolverReducer } from './reducer'; -export const storeFactory = () => { - /** - * Redux Devtools extension exposes itself via a property on the global object. - * This interface can be used to cast `window` to a type that may expose Redux Devtools. - */ - interface SomethingThatMightHaveReduxDevTools { - __REDUX_DEVTOOLS_EXTENSION__?: (options?: PartialReduxDevToolsOptions) => StoreEnhancer; - } +export const storeFactory = (): { store: Store<ResolverState, ResolverAction> } => { + const actionsBlacklist: Array<ResolverAction['type']> = ['userMovedPointer']; + const composeEnhancers = composeWithDevTools({ + name: 'Resolver', + actionsBlacklist, + }); - /** - * Some of the options that can be passed when configuring Redux Devtools. - */ - interface PartialReduxDevToolsOptions { - /** - * A name for this store - */ - name?: string; - /** - * A list of action types to ignore. This is used to ignore high frequency events created by a mousemove handler - */ - actionsBlacklist?: readonly string[]; - } - const windowWhichMightHaveReduxDevTools = window as SomethingThatMightHaveReduxDevTools; - // Make sure blacklisted action types are valid - const actionsBlacklist: ReadonlyArray<ResolverAction['type']> = ['userMovedPointer']; - const store = createStore( - resolverReducer, - windowWhichMightHaveReduxDevTools.__REDUX_DEVTOOLS_EXTENSION__ && - windowWhichMightHaveReduxDevTools.__REDUX_DEVTOOLS_EXTENSION__({ - name: 'Resolver', - actionsBlacklist, - }) - ); + const middlewareEnhancer = applyMiddleware(); + + const store = createStore(resolverReducer, composeEnhancers(middlewareEnhancer)); return { store, }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts new file mode 100644 index 00000000000000..8808160c9c631a --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts @@ -0,0 +1,30 @@ +/* + * 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 { animatePanning } from './camera/methods'; +import { processNodePositionsAndEdgeLineSegments } from './selectors'; +import { ResolverState, ProcessEvent } from '../types'; + +const animationDuration = 1000; + +/** + * Return new `ResolverState` with the camera animating to focus on `process`. + */ +export function animateProcessIntoView( + state: ResolverState, + startTime: number, + process: ProcessEvent +): ResolverState { + const { processNodePositions } = processNodePositionsAndEdgeLineSegments(state); + const position = processNodePositions.get(process); + if (position) { + return { + ...state, + camera: animatePanning(state.camera, startTime, position, animationDuration), + }; + } + return state; +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts index 97ab51cbd6deaa..20c490b8998f91 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts @@ -4,11 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ import { Reducer, combineReducers } from 'redux'; +import { animateProcessIntoView } from './methods'; import { cameraReducer } from './camera/reducer'; import { dataReducer } from './data/reducer'; import { ResolverState, ResolverAction } from '../types'; -export const resolverReducer: Reducer<ResolverState, ResolverAction> = combineReducers({ +const concernReducers = combineReducers({ camera: cameraReducer, data: dataReducer, }); + +export const resolverReducer: Reducer<ResolverState, ResolverAction> = (state, action) => { + const nextState = concernReducers(state, action); + if (action.type === 'userBroughtProcessIntoView') { + return animateProcessIntoView(nextState, action.payload.time, action.payload.process); + } else { + return nextState; + } +}; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index eb1c1fec369957..4d12e656205fae 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -17,6 +17,9 @@ export const projectionMatrix = composeSelectors( cameraSelectors.projectionMatrix ); +export const clippingPlanes = composeSelectors(cameraStateSelector, cameraSelectors.clippingPlanes); +export const translation = composeSelectors(cameraStateSelector, cameraSelectors.translation); + /** * A matrix that when applied to a Vector2 converts it from screen coordinates to world coordinates. * See https://en.wikipedia.org/wiki/Orthographic_projection @@ -28,6 +31,7 @@ export const inverseProjectionMatrix = composeSelectors( /** * The scale by which world values are scaled when rendered. + * TODO make it a number */ export const scale = composeSelectors(cameraStateSelector, cameraSelectors.scale); @@ -41,6 +45,11 @@ export const scalingFactor = composeSelectors(cameraStateSelector, cameraSelecto */ export const userIsPanning = composeSelectors(cameraStateSelector, cameraSelectors.userIsPanning); +/** + * Whether or not the camera is animating, at a given time. + */ +export const isAnimating = composeSelectors(cameraStateSelector, cameraSelectors.isAnimating); + export const processNodePositionsAndEdgeLineSegments = composeSelectors( dataStateSelector, dataSelectors.processNodePositionsAndEdgeLineSegments diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index f2ae9785446f7a..6c6936d377deac 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ResolverAction } from './actions'; +import { Store } from 'redux'; + +import { ResolverAction } from './store/actions'; +export { ResolverAction } from './store/actions'; /** * Redux state for the Resolver feature. Properties on this interface are populated via multiple reducers using redux's `combineReducers`. @@ -21,27 +24,34 @@ export interface ResolverState { readonly data: DataState; } -interface PanningState { +/** + * Piece of redux state that models an animation for the camera. + */ +export interface CameraAnimationState { + /** + * The time when the animation began. + */ + readonly startTime: number; /** - * Screen coordinate vector representing the starting point when panning. + * The final translation when the animation is complete. */ - readonly origin: Vector2; + readonly targetTranslation: Vector2; + /** + * The effective camera position (including an in-progress user panning) at the time + * when the animation began. + */ + readonly initialTranslation: Vector2; /** - * Screen coordinate vector representing the current point when panning. + * The duration, in milliseconds, that the animation should last. Should be > 0 */ - readonly currentOffset: Vector2; + readonly duration: number; } /** - * Redux state for the virtual 'camera' used by Resolver. + * The redux state for the `useCamera` hook. */ -export interface CameraState { - /** - * Contains the starting and current position of the pointer when the user is panning the map. - */ - readonly panning?: PanningState; - +export type CameraState = { /** * Scales the coordinate system, used for zooming. Should always be between 0 and 1 */ @@ -54,7 +64,7 @@ export interface CameraState { /** * The camera world transform not counting any change from panning. When panning finishes, this value is updated to account for it. - * Use the `transform` selector to get the transform adjusted for panning. + * Use the `translation` selector to get the effective translation adjusted for panning. */ readonly translationNotCountingCurrentPanning: Vector2; @@ -62,7 +72,43 @@ export interface CameraState { * The world coordinates that the pointing device was last over. This is used during mousewheel zoom. */ readonly latestFocusedWorldCoordinates: Vector2 | null; -} +} & ( + | { + /** + * Contains the animation start time and target translation. This doesn't model the instantaneous + * progress of an animation. Instead, animation is model as functions-of-time. + */ + readonly animation: CameraAnimationState; + /** + * If the camera is animating, it must not be panning. + */ + readonly panning: undefined; + } + | { + /** + * If the camera is panning, it must not be animating. + */ + readonly animation: undefined; + /** + * Contains the starting and current position of the pointer when the user is panning the map. + */ + readonly panning: { + /** + * Screen coordinate vector representing the starting point when panning. + */ + readonly origin: Vector2; + + /** + * Screen coordinate vector representing the current point when panning. + */ + readonly currentOffset: Vector2; + }; + } + | { + readonly animation: undefined; + readonly panning: undefined; + } +); /** * State for `data` reducer which handles receiving Resolver data from the backend. @@ -73,8 +119,6 @@ export interface DataState { export type Vector2 = readonly [number, number]; -export type Vector3 = readonly [number, number, number]; - /** * A rectangle with sides that align with the `x` and `y` axises. */ @@ -121,6 +165,7 @@ export interface ProcessEvent { readonly event_type: number; readonly machine_id: string; readonly data_buffer: { + timestamp_utc: string; event_subtype_full: eventSubtypeFull; event_type_full: eventTypeFull; node_id: number; @@ -184,6 +229,48 @@ export type ProcessWithWidthMetadata = { ); /** - * String that represents the direction in which Resolver can be panned + * The constructor for a ResizeObserver */ -export type PanDirection = 'north' | 'south' | 'east' | 'west'; +interface ResizeObserverConstructor { + prototype: ResizeObserver; + new (callback: ResizeObserverCallback): ResizeObserver; +} + +/** + * Functions that introduce side effects. A React context provides these, and they may be mocked in tests. + */ +export interface SideEffectors { + /** + * A function which returns the time since epoch in milliseconds. Injected because mocking Date is tedious. + */ + timestamp: () => number; + requestAnimationFrame: typeof window.requestAnimationFrame; + cancelAnimationFrame: typeof window.cancelAnimationFrame; + ResizeObserver: ResizeObserverConstructor; +} + +export interface SideEffectSimulator { + /** + * Control the mock `SideEffectors`. + */ + controls: { + /** + * Set or get the `time` number used for `timestamp` and `requestAnimationFrame` callbacks. + */ + time: number; + /** + * Call any pending `requestAnimationFrame` callbacks. + */ + provideAnimationFrame: () => void; + /** + * Trigger `ResizeObserver` callbacks for `element` and update the mocked value for `getBoundingClientRect`. + */ + simulateElementResize: (element: Element, contentRect: DOMRect) => void; + }; + /** + * Mocked `SideEffectors`. + */ + mock: jest.Mocked<Omit<SideEffectors, 'ResizeObserver'>> & Pick<SideEffectors, 'ResizeObserver'>; +} + +export type ResolverStore = Store<ResolverState, ResolverAction>; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx index cdecd3e02bde10..3386ed4a448d53 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx @@ -6,10 +6,8 @@ import React from 'react'; import styled from 'styled-components'; -import { useSelector } from 'react-redux'; import { applyMatrix3, distance, angle } from '../lib/vector2'; -import { Vector2 } from '../types'; -import * as selectors from '../store/selectors'; +import { Vector2, Matrix3 } from '../types'; /** * A placeholder line segment view that connects process nodes. @@ -20,6 +18,7 @@ export const EdgeLine = styled( className, startPosition, endPosition, + projectionMatrix, }: { /** * A className string provided by `styled` @@ -33,12 +32,15 @@ export const EdgeLine = styled( * The postion of second point in the line segment. In 'world' coordinates. */ endPosition: Vector2; + /** + * projectionMatrix which can be used to convert `startPosition` and `endPosition` to screen coordinates. + */ + projectionMatrix: Matrix3; }) => { /** * Convert the start and end positions, which are in 'world' coordinates, * to `left` and `top` css values. */ - const projectionMatrix = useSelector(selectors.projectionMatrix); const screenStart = applyMatrix3(startPosition, projectionMatrix); const screenEnd = applyMatrix3(endPosition, projectionMatrix); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/graph_controls.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/graph_controls.tsx index 3170f8bdf867ea..a1cd003949a22f 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/graph_controls.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo, useContext } from 'react'; import styled from 'styled-components'; import { EuiRange, EuiPanel, EuiIcon } from '@elastic/eui'; import { useSelector, useDispatch } from 'react-redux'; -import { ResolverAction, PanDirection } from '../types'; +import { SideEffectContext } from './side_effect_context'; +import { ResolverAction, Vector2 } from '../types'; import * as selectors from '../store/selectors'; /** @@ -26,6 +27,7 @@ export const GraphControls = styled( }) => { const dispatch: (action: ResolverAction) => unknown = useDispatch(); const scalingFactor = useSelector(selectors.scalingFactor); + const { timestamp } = useContext(SideEffectContext); const handleZoomAmountChange = useCallback( (event: React.ChangeEvent<HTMLInputElement> | React.MouseEvent<HTMLButtonElement>) => { @@ -61,36 +63,45 @@ export const GraphControls = styled( }); }, [dispatch]); - const handlePanClick = (panDirection: PanDirection) => { - return () => { - dispatch({ - type: 'userClickedPanControl', - payload: panDirection, - }); - }; - }; + const [handleNorth, handleEast, handleSouth, handleWest] = useMemo(() => { + const directionVectors: readonly Vector2[] = [ + [0, 1], + [1, 0], + [0, -1], + [-1, 0], + ]; + return directionVectors.map(direction => { + return () => { + const action: ResolverAction = { + type: 'userNudgedCamera', + payload: { direction, time: timestamp() }, + }; + dispatch(action); + }; + }); + }, [dispatch, timestamp]); return ( <div className={className}> <EuiPanel className="panning-controls" paddingSize="none" hasShadow> <div className="panning-controls-top"> - <button className="north-button" title="North" onClick={handlePanClick('north')}> + <button className="north-button" title="North" onClick={handleNorth}> <EuiIcon type="arrowUp" /> </button> </div> <div className="panning-controls-middle"> - <button className="west-button" title="West" onClick={handlePanClick('west')}> + <button className="west-button" title="West" onClick={handleWest}> <EuiIcon type="arrowLeft" /> </button> <button className="center-button" title="Center" onClick={handleCenterClick}> <EuiIcon type="bullseye" /> </button> - <button className="east-button" title="East" onClick={handlePanClick('east')}> + <button className="east-button" title="East" onClick={handleEast}> <EuiIcon type="arrowRight" /> </button> </div> <div className="panning-controls-bottom"> - <button className="south-button" title="South" onClick={handlePanClick('south')}> + <button className="south-button" title="South" onClick={handleSouth}> <EuiIcon type="arrowDown" /> </button> </div> @@ -116,10 +127,6 @@ export const GraphControls = styled( } ) )` - position: absolute; - top: 5px; - left: 5px; - z-index: 1; background-color: #d4d4d4; color: #333333; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx index a69504e3a5db12..d71a4d87b7eab6 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -4,151 +4,62 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useState, useEffect } from 'react'; -import { Store } from 'redux'; -import { Provider, useSelector, useDispatch } from 'react-redux'; +import React from 'react'; +import { useSelector } from 'react-redux'; import styled from 'styled-components'; -import { ResolverState, ResolverAction } from '../types'; import * as selectors from '../store/selectors'; -import { useAutoUpdatingClientRect } from './use_autoupdating_client_rect'; -import { useNonPassiveWheelHandler } from './use_nonpassive_wheel_handler'; -import { ProcessEventDot } from './process_event_dot'; import { EdgeLine } from './edge_line'; +import { Panel } from './panel'; import { GraphControls } from './graph_controls'; +import { ProcessEventDot } from './process_event_dot'; +import { useCamera } from './use_camera'; + +const StyledPanel = styled(Panel)` + position: absolute; + left: 1em; + top: 1em; + max-height: calc(100% - 2em); + overflow: auto; + width: 25em; + max-width: 50%; +`; -export const AppRoot = React.memo(({ store }: { store: Store<ResolverState, ResolverAction> }) => { - return ( - <Provider store={store}> - <Resolver /> - </Provider> - ); -}); - -const Resolver = styled( - React.memo(({ className }: { className?: string }) => { - const dispatch: (action: ResolverAction) => unknown = useDispatch(); +const StyledGraphControls = styled(GraphControls)` + position: absolute; + top: 5px; + right: 5px; +`; +export const Resolver = styled( + React.memo(function Resolver({ className }: { className?: string }) { const { processNodePositions, edgeLineSegments } = useSelector( selectors.processNodePositionsAndEdgeLineSegments ); - const [ref, setRef] = useState<null | HTMLDivElement>(null); - - const userIsPanning = useSelector(selectors.userIsPanning); - - const [elementBoundingClientRect, clientRectCallback] = useAutoUpdatingClientRect(); - - const relativeCoordinatesFromMouseEvent = useCallback( - (event: { clientX: number; clientY: number }): null | [number, number] => { - if (elementBoundingClientRect === null) { - return null; - } - return [ - event.clientX - elementBoundingClientRect.x, - event.clientY - elementBoundingClientRect.y, - ]; - }, - [elementBoundingClientRect] - ); - - useEffect(() => { - if (elementBoundingClientRect !== null) { - dispatch({ - type: 'userSetRasterSize', - payload: [elementBoundingClientRect.width, elementBoundingClientRect.height], - }); - } - }, [dispatch, elementBoundingClientRect]); - - const handleMouseDown = useCallback( - (event: React.MouseEvent<HTMLDivElement>) => { - const maybeCoordinates = relativeCoordinatesFromMouseEvent(event); - if (maybeCoordinates !== null) { - dispatch({ - type: 'userStartedPanning', - payload: maybeCoordinates, - }); - } - }, - [dispatch, relativeCoordinatesFromMouseEvent] - ); - - const handleMouseMove = useCallback( - (event: MouseEvent) => { - const maybeCoordinates = relativeCoordinatesFromMouseEvent(event); - if (maybeCoordinates) { - dispatch({ - type: 'userMovedPointer', - payload: maybeCoordinates, - }); - } - }, - [dispatch, relativeCoordinatesFromMouseEvent] - ); - - const handleMouseUp = useCallback(() => { - if (userIsPanning) { - dispatch({ - type: 'userStoppedPanning', - }); - } - }, [dispatch, userIsPanning]); - - const handleWheel = useCallback( - (event: WheelEvent) => { - if ( - elementBoundingClientRect !== null && - event.ctrlKey && - event.deltaY !== 0 && - event.deltaMode === 0 - ) { - event.preventDefault(); - dispatch({ - type: 'userZoomed', - // we use elementBoundingClientRect to interpret pixel deltas as a fraction of the element's height - // when pinch-zooming in on a mac, deltaY is a negative number but we want the payload to be positive - payload: event.deltaY / -elementBoundingClientRect.height, - }); - } - }, - [elementBoundingClientRect, dispatch] - ); - - useEffect(() => { - window.addEventListener('mouseup', handleMouseUp, { passive: true }); - return () => { - window.removeEventListener('mouseup', handleMouseUp); - }; - }, [handleMouseUp]); - - useEffect(() => { - window.addEventListener('mousemove', handleMouseMove, { passive: true }); - return () => { - window.removeEventListener('mousemove', handleMouseMove); - }; - }, [handleMouseMove]); - - const refCallback = useCallback( - (node: null | HTMLDivElement) => { - setRef(node); - clientRectCallback(node); - }, - [clientRectCallback] - ); - - useNonPassiveWheelHandler(handleWheel, ref); + const { projectionMatrix, ref, onMouseDown } = useCamera(); return ( <div data-test-subj="resolverEmbeddable" className={className}> - <GraphControls /> - <div className="resolver-graph" onMouseDown={handleMouseDown} ref={refCallback}> + <div className="resolver-graph" onMouseDown={onMouseDown} ref={ref}> {Array.from(processNodePositions).map(([processEvent, position], index) => ( - <ProcessEventDot key={index} position={position} event={processEvent} /> + <ProcessEventDot + key={index} + position={position} + projectionMatrix={projectionMatrix} + event={processEvent} + /> ))} {edgeLineSegments.map(([startPosition, endPosition], index) => ( - <EdgeLine key={index} startPosition={startPosition} endPosition={endPosition} /> + <EdgeLine + key={index} + startPosition={startPosition} + endPosition={endPosition} + projectionMatrix={projectionMatrix} + /> ))} </div> + <StyledPanel /> + <StyledGraphControls /> </div> ); }) @@ -156,8 +67,11 @@ const Resolver = styled( /** * Take up all availble space */ - display: flex; - flex-grow: 1; + &, + .resolver-graph { + display: flex; + flex-grow: 1; + } /** * The placeholder components use absolute positioning. */ @@ -166,9 +80,4 @@ const Resolver = styled( * Prevent partially visible components from showing up outside the bounds of Resolver. */ overflow: hidden; - - .resolver-graph { - display: flex; - flex-grow: 1; - } `; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx new file mode 100644 index 00000000000000..c75b73b4bceafd --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx @@ -0,0 +1,165 @@ +/* + * 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, useCallback, useMemo, useContext } from 'react'; +import { EuiPanel, EuiBadge, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiTitle } from '@elastic/eui'; +import { EuiHorizontalRule, EuiInMemoryTable } from '@elastic/eui'; +import euiVars from '@elastic/eui/dist/eui_theme_light.json'; +import { useSelector } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { SideEffectContext } from './side_effect_context'; +import { ProcessEvent } from '../types'; +import { useResolverDispatch } from './use_resolver_dispatch'; +import * as selectors from '../store/selectors'; + +const HorizontalRule = memo(function HorizontalRule() { + return ( + <EuiHorizontalRule + style={{ + /** + * Cannot use `styled` to override this because the specificity of EuiHorizontalRule's + * CSS selectors is too high. + */ + marginLeft: `-${euiVars.euiPanelPaddingModifiers.paddingMedium}`, + marginRight: `-${euiVars.euiPanelPaddingModifiers.paddingMedium}`, + /** + * The default width is 100%, but this should be greater. + */ + width: 'auto', + }} + /> + ); +}); + +export const Panel = memo(function Event({ className }: { className?: string }) { + interface ProcessTableView { + name: string; + timestamp?: Date; + event: ProcessEvent; + } + + const { processNodePositions } = useSelector(selectors.processNodePositionsAndEdgeLineSegments); + const { timestamp } = useContext(SideEffectContext); + + const processTableView: ProcessTableView[] = useMemo( + () => + [...processNodePositions.keys()].map(processEvent => { + const { data_buffer } = processEvent; + const date = new Date(data_buffer.timestamp_utc); + return { + name: data_buffer.process_name, + timestamp: isFinite(date.getTime()) ? date : undefined, + event: processEvent, + }; + }), + [processNodePositions] + ); + + const formatter = new Intl.DateTimeFormat(i18n.getLocale(), { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); + + const dispatch = useResolverDispatch(); + + const handleBringIntoViewClick = useCallback( + processTableViewItem => { + dispatch({ + type: 'userBroughtProcessIntoView', + payload: { + time: timestamp(), + process: processTableViewItem.event, + }, + }); + }, + [dispatch, timestamp] + ); + + const columns = useMemo<Array<EuiBasicTableColumn<ProcessTableView>>>( + () => [ + { + field: 'name', + name: i18n.translate('xpack.endpoint.resolver.panel.tabel.row.processNameTitle', { + defaultMessage: 'Process Name', + }), + sortable: true, + truncateText: true, + render(name: string) { + return name === '' ? ( + <EuiBadge color="warning"> + {i18n.translate('xpack.endpoint.resolver.panel.table.row.valueMissingDescription', { + defaultMessage: 'Value is missing', + })} + </EuiBadge> + ) : ( + name + ); + }, + }, + { + field: 'timestamp', + name: i18n.translate('xpack.endpoint.resolver.panel.tabel.row.timestampTitle', { + defaultMessage: 'Timestamp', + }), + dataType: 'date', + sortable: true, + render(eventTimestamp?: Date) { + return eventTimestamp ? ( + formatter.format(eventTimestamp) + ) : ( + <EuiBadge color="warning"> + {i18n.translate('xpack.endpoint.resolver.panel.tabel.row.timestampInvalidLabel', { + defaultMessage: 'invalid', + })} + </EuiBadge> + ); + }, + }, + { + name: i18n.translate('xpack.endpoint.resolver.panel.tabel.row.actionsTitle', { + defaultMessage: 'Actions', + }), + actions: [ + { + name: i18n.translate( + 'xpack.endpoint.resolver.panel.tabel.row.actions.bringIntoViewButtonLabel', + { + defaultMessage: 'Bring into view', + } + ), + description: i18n.translate( + 'xpack.endpoint.resolver.panel.tabel.row.bringIntoViewLabel', + { + defaultMessage: 'Bring the process into view on the map.', + } + ), + type: 'icon', + icon: 'flag', + onClick: handleBringIntoViewClick, + }, + ], + }, + ], + [formatter, handleBringIntoViewClick] + ); + return ( + <EuiPanel className={className}> + <EuiTitle size="xs"> + <h4> + {i18n.translate('xpack.endpoint.resolver.panel.title', { + defaultMessage: 'Processes', + })} + </h4> + </EuiTitle> + <HorizontalRule /> + <EuiInMemoryTable<ProcessTableView> items={processTableView} columns={columns} sorting /> + </EuiPanel> + ); +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx index 5c3a253d619ef8..384fbf90ed9847 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx @@ -6,10 +6,8 @@ import React from 'react'; import styled from 'styled-components'; -import { useSelector } from 'react-redux'; import { applyMatrix3 } from '../lib/vector2'; -import { Vector2, ProcessEvent } from '../types'; -import * as selectors from '../store/selectors'; +import { Vector2, ProcessEvent, Matrix3 } from '../types'; /** * A placeholder view for a process node. @@ -20,6 +18,7 @@ export const ProcessEventDot = styled( className, position, event, + projectionMatrix, }: { /** * A `className` string provided by `styled` @@ -33,12 +32,16 @@ export const ProcessEventDot = styled( * An event which contains details about the process node. */ event: ProcessEvent; + /** + * projectionMatrix which can be used to convert `position` to screen coordinates. + */ + projectionMatrix: Matrix3; }) => { /** * Convert the position, which is in 'world' coordinates, to screen coordinates. */ - const projectionMatrix = useSelector(selectors.projectionMatrix); const [left, top] = applyMatrix3(position, projectionMatrix); + const style = { left: (left - 20).toString() + 'px', top: (top - 20).toString() + 'px', diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/side_effect_context.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/view/side_effect_context.ts new file mode 100644 index 00000000000000..ab7f41d8150268 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/side_effect_context.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { createContext, Context } from 'react'; +import ResizeObserver from 'resize-observer-polyfill'; +import { SideEffectors } from '../types'; + +/** + * React context that provides 'side-effectors' which we need to mock during testing. + */ +const sideEffectors: SideEffectors = { + timestamp: () => Date.now(), + requestAnimationFrame(...args) { + return window.requestAnimationFrame(...args); + }, + cancelAnimationFrame(...args) { + return window.cancelAnimationFrame(...args); + }, + ResizeObserver, +}; + +/** + * The default values are used in production, tests can provide mock values using `SideEffectSimulator`. + */ +export const SideEffectContext: Context<SideEffectors> = createContext(sideEffectors); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/side_effect_simulator.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/view/side_effect_simulator.ts new file mode 100644 index 00000000000000..3e80b6a8459f7c --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/side_effect_simulator.ts @@ -0,0 +1,170 @@ +/* + * 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 { act } from '@testing-library/react'; +import { SideEffectSimulator } from '../types'; + +/** + * Create mock `SideEffectors` for `SideEffectContext.Provider`. The `control` + * object is used to control the mocks. + */ +export const sideEffectSimulator: () => SideEffectSimulator = () => { + // The set of mock `ResizeObserver` instances that currently exist + const resizeObserverInstances: Set<MockResizeObserver> = new Set(); + + // A map of `Element`s to their fake `DOMRect`s + const contentRects: Map<Element, DOMRect> = new Map(); + + /** + * Simulate an element's size changing. This will trigger any `ResizeObserverCallback`s which + * are listening for this element's size changes. It will also cause `element.getBoundingClientRect` to + * return `contentRect` + */ + const simulateElementResize: (target: Element, contentRect: DOMRect) => void = ( + target, + contentRect + ) => { + contentRects.set(target, contentRect); + for (const instance of resizeObserverInstances) { + instance.simulateElementResize(target, contentRect); + } + }; + + /** + * Get the simulate `DOMRect` for `element`. + */ + const contentRectForElement: (target: Element) => DOMRect = target => { + if (contentRects.has(target)) { + return contentRects.get(target)!; + } + const domRect: DOMRect = { + x: 0, + y: 0, + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 0, + height: 0, + toJSON() { + return this; + }, + }; + return domRect; + }; + + /** + * Change `Element.prototype.getBoundingClientRect` to return our faked values. + */ + jest + .spyOn(Element.prototype, 'getBoundingClientRect') + .mockImplementation(function(this: Element) { + return contentRectForElement(this); + }); + + /** + * A mock implementation of `ResizeObserver` that works with our fake `getBoundingClientRect` and `simulateElementResize` + */ + class MockResizeObserver implements ResizeObserver { + constructor(private readonly callback: ResizeObserverCallback) { + resizeObserverInstances.add(this); + } + private elements: Set<Element> = new Set(); + /** + * Simulate `target` changing it size to `contentRect`. + */ + simulateElementResize(target: Element, contentRect: DOMRect) { + if (this.elements.has(target)) { + const entries: ResizeObserverEntry[] = [{ target, contentRect }]; + this.callback(entries, this); + } + } + observe(target: Element) { + this.elements.add(target); + } + unobserve(target: Element) { + this.elements.delete(target); + } + disconnect() { + this.elements.clear(); + } + } + + /** + * milliseconds since epoch, faked. + */ + let mockTime: number = 0; + + /** + * A counter allowing us to give a unique ID for each call to `requestAnimationFrame`. + */ + let frameRequestedCallbacksIDCounter: number = 0; + + /** + * A map of requestAnimationFrame IDs to the related callbacks. + */ + const frameRequestedCallbacks: Map<number, FrameRequestCallback> = new Map(); + + /** + * Trigger any pending `requestAnimationFrame` callbacks. Passes `mockTime` as the timestamp. + */ + const provideAnimationFrame: () => void = () => { + act(() => { + // Iterate the values, and clear the data set before calling the callbacks because the callbacks will repopulate the dataset synchronously in this testing framework. + const values = [...frameRequestedCallbacks.values()]; + frameRequestedCallbacks.clear(); + for (const callback of values) { + callback(mockTime); + } + }); + }; + + /** + * Provide a fake ms timestamp + */ + const timestamp = jest.fn(() => mockTime); + + /** + * Fake `requestAnimationFrame`. + */ + const requestAnimationFrame = jest.fn((callback: FrameRequestCallback): number => { + const id = frameRequestedCallbacksIDCounter++; + frameRequestedCallbacks.set(id, callback); + return id; + }); + + /** + * fake `cancelAnimationFrame`. + */ + const cancelAnimationFrame = jest.fn((id: number) => { + frameRequestedCallbacks.delete(id); + }); + + const retval: SideEffectSimulator = { + controls: { + provideAnimationFrame, + + /** + * Change the mock time value + */ + set time(nextTime: number) { + mockTime = nextTime; + }, + get time() { + return mockTime; + }, + + simulateElementResize, + }, + mock: { + requestAnimationFrame, + cancelAnimationFrame, + timestamp, + ResizeObserver: MockResizeObserver, + }, + }; + return retval; +}; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx deleted file mode 100644 index 5f13995de1c2ad..00000000000000 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx +++ /dev/null @@ -1,43 +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 { useCallback, useState, useEffect, useRef } from 'react'; -import ResizeObserver from 'resize-observer-polyfill'; - -/** - * Returns a nullable DOMRect and a ref callback. Pass the refCallback to the - * `ref` property of a native element and this hook will return a DOMRect for - * it by calling `getBoundingClientRect`. This hook will observe the element - * with a resize observer and call getBoundingClientRect again after resizes. - * - * Note that the changes to the position of the element aren't automatically - * tracked. So if the element's position moves for some reason, be sure to - * handle that. - */ -export function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | null) => void] { - const [rect, setRect] = useState<DOMRect | null>(null); - const nodeRef = useRef<Element | null>(null); - const ref = useCallback((node: Element | null) => { - nodeRef.current = node; - if (node !== null) { - setRect(node.getBoundingClientRect()); - } - }, []); - useEffect(() => { - if (nodeRef.current !== null) { - const resizeObserver = new ResizeObserver(entries => { - if (nodeRef.current !== null && nodeRef.current === entries[0].target) { - setRect(nodeRef.current.getBoundingClientRect()); - } - }); - resizeObserver.observe(nodeRef.current); - return () => { - resizeObserver.disconnect(); - }; - } - }, [nodeRef]); - return [rect, ref]; -} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx new file mode 100644 index 00000000000000..85e1d4e694b150 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx @@ -0,0 +1,197 @@ +/* + * 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. + */ + +/** + * This import must be hoisted as it uses `jest.mock`. Is there a better way? Mocking is not good. + */ +import React from 'react'; +import { render, act, RenderResult, fireEvent } from '@testing-library/react'; +import { useCamera } from './use_camera'; +import { Provider } from 'react-redux'; +import * as selectors from '../store/selectors'; +import { storeFactory } from '../store'; +import { + Matrix3, + ResolverAction, + ResolverStore, + ProcessEvent, + SideEffectSimulator, +} from '../types'; +import { SideEffectContext } from './side_effect_context'; +import { applyMatrix3 } from '../lib/vector2'; +import { sideEffectSimulator } from './side_effect_simulator'; + +describe('useCamera on an unpainted element', () => { + let element: HTMLElement; + let projectionMatrix: Matrix3; + const testID = 'camera'; + let reactRenderResult: RenderResult; + let store: ResolverStore; + let simulator: SideEffectSimulator; + beforeEach(async () => { + ({ store } = storeFactory()); + + const Test = function Test() { + const camera = useCamera(); + const { ref, onMouseDown } = camera; + projectionMatrix = camera.projectionMatrix; + return <div data-testid={testID} onMouseDown={onMouseDown} ref={ref} />; + }; + + simulator = sideEffectSimulator(); + + reactRenderResult = render( + <Provider store={store}> + <SideEffectContext.Provider value={simulator.mock}> + <Test /> + </SideEffectContext.Provider> + </Provider> + ); + + const { findByTestId } = reactRenderResult; + element = await findByTestId(testID); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should be usable in React', async () => { + expect(element).toBeInTheDocument(); + }); + test('returns a projectionMatrix that changes everything to 0', () => { + expect(applyMatrix3([0, 0], projectionMatrix)).toEqual([0, 0]); + }); + describe('which has been resized to 800x600', () => { + const width = 800; + const height = 600; + const leftMargin = 20; + const topMargin = 20; + const centerX = width / 2 + leftMargin; + const centerY = height / 2 + topMargin; + beforeEach(() => { + act(() => { + simulator.controls.simulateElementResize(element, { + width, + height, + left: leftMargin, + top: topMargin, + right: leftMargin + width, + bottom: topMargin + height, + x: leftMargin, + y: topMargin, + toJSON() { + return this; + }, + }); + }); + }); + test('provides a projection matrix that inverts the y axis and translates 400,300 (center of the element)', () => { + expect(applyMatrix3([0, 0], projectionMatrix)).toEqual([400, 300]); + }); + describe('when the user presses the mousedown button in the middle of the element', () => { + beforeEach(() => { + fireEvent.mouseDown(element, { + clientX: centerX, + clientY: centerY, + }); + }); + describe('when the user moves the mouse 50 pixels to the right', () => { + beforeEach(() => { + fireEvent.mouseMove(element, { + clientX: centerX + 50, + clientY: centerY, + }); + }); + it('should project [0, 0] in world corrdinates 50 pixels to the right of the center of the element', () => { + expect(applyMatrix3([0, 0], projectionMatrix)).toEqual([450, 300]); + }); + }); + }); + + describe('when the user uses the mousewheel w/ ctrl held down', () => { + beforeEach(() => { + fireEvent.wheel(element, { + ctrlKey: true, + deltaY: -10, + deltaMode: 0, + }); + }); + it('should zoom in', () => { + expect(projectionMatrix).toMatchInlineSnapshot(` + Array [ + 1.0635255481707058, + 0, + 400, + 0, + -1.0635255481707058, + 300, + 0, + 0, + 0, + ] + `); + }); + }); + + it('should not initially request an animation frame', () => { + expect(simulator.mock.requestAnimationFrame).not.toHaveBeenCalled(); + }); + describe('when the camera begins animation', () => { + let process: ProcessEvent; + beforeEach(() => { + // At this time, processes are provided via mock data. In the future, this test will have to provide those mocks. + const processes: ProcessEvent[] = [ + ...selectors + .processNodePositionsAndEdgeLineSegments(store.getState()) + .processNodePositions.keys(), + ]; + process = processes[processes.length - 1]; + simulator.controls.time = 0; + const action: ResolverAction = { + type: 'userBroughtProcessIntoView', + payload: { + time: simulator.controls.time, + process, + }, + }; + act(() => { + store.dispatch(action); + }); + }); + + it('should request animation frames in a loop', () => { + const animationDuration = 1000; + // When the animation begins, the camera should request an animation frame. + expect(simulator.mock.requestAnimationFrame).toHaveBeenCalledTimes(1); + + // Update the time so that the animation is partially complete. + simulator.controls.time = animationDuration / 5; + // Provide the animation frame, allowing the camera to rerender. + simulator.controls.provideAnimationFrame(); + + // The animation is not complete, so the camera should request another animation frame. + expect(simulator.mock.requestAnimationFrame).toHaveBeenCalledTimes(2); + + // Update the camera so that the animation is nearly complete. + simulator.controls.time = (animationDuration / 10) * 9; + + // Provide the animation frame + simulator.controls.provideAnimationFrame(); + + // Since the animation isn't complete, it should request another frame + expect(simulator.mock.requestAnimationFrame).toHaveBeenCalledTimes(3); + + // Animation lasts 1000ms, so this should end it + simulator.controls.time = animationDuration * 1.1; + + // Provide the last frame + simulator.controls.provideAnimationFrame(); + + // Since animation is complete, it should not have requseted another frame + expect(simulator.mock.requestAnimationFrame).toHaveBeenCalledTimes(3); + }); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.ts new file mode 100644 index 00000000000000..54940b8383f7a8 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.ts @@ -0,0 +1,307 @@ +/* + * 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, { + useCallback, + useState, + useEffect, + useRef, + useLayoutEffect, + useContext, +} from 'react'; +import { useSelector } from 'react-redux'; +import { SideEffectContext } from './side_effect_context'; +import { Matrix3 } from '../types'; +import { useResolverDispatch } from './use_resolver_dispatch'; +import * as selectors from '../store/selectors'; + +export function useCamera(): { + /** + * A function to pass to a React element's `ref` property. Used to attach + * native event listeners and to measure the DOM node. + */ + ref: (node: HTMLDivElement | null) => void; + onMouseDown: React.MouseEventHandler<HTMLElement>; + /** + * A 3x3 transformation matrix used to convert a `vector2` from 'world' coordinates + * to screen coordinates. + */ + projectionMatrix: Matrix3; +} { + const dispatch = useResolverDispatch(); + const sideEffectors = useContext(SideEffectContext); + + const [ref, setRef] = useState<null | HTMLDivElement>(null); + + /** + * The position of a thing, as a `Vector2`, is multiplied by the projection matrix + * to determine where it belongs on the screen. + * The projection matrix changes over time if the camera is currently animating. + */ + const projectionMatrixAtTime = useSelector(selectors.projectionMatrix); + + /** + * Use a ref to refer to the `projectionMatrixAtTime` function. The rAF loop + * accesses this and sets state during the rAF cycle. If the rAF loop + * effect read this directly from the selector, the rAF loop would need to + * be re-inited each time this function changed. The `projectionMatrixAtTime` function + * changes each frame during an animation, so the rAF loop would be causing + * itself to reinit on each frame. This would necessarily cause a drop in FPS as there + * would be a dead zone between when the rAF loop stopped and restarted itself. + */ + const projectionMatrixAtTimeRef = useRef<typeof projectionMatrixAtTime>(); + + /** + * The projection matrix is stateful, depending on the current time. + * When the projection matrix changes, the component should be rerendered. + */ + const [projectionMatrix, setProjectionMatrix] = useState<Matrix3>( + projectionMatrixAtTime(sideEffectors.timestamp()) + ); + + const userIsPanning = useSelector(selectors.userIsPanning); + const isAnimatingAtTime = useSelector(selectors.isAnimating); + + const [elementBoundingClientRect, clientRectCallback] = useAutoUpdatingClientRect(); + + /** + * For an event with clientX and clientY, return [clientX, clientY] - the top left corner of the `ref` element + */ + const relativeCoordinatesFromMouseEvent = useCallback( + (event: { clientX: number; clientY: number }): null | [number, number] => { + if (elementBoundingClientRect === null) { + return null; + } + return [ + event.clientX - elementBoundingClientRect.x, + event.clientY - elementBoundingClientRect.y, + ]; + }, + [elementBoundingClientRect] + ); + + const handleMouseDown = useCallback( + (event: React.MouseEvent<HTMLDivElement>) => { + const maybeCoordinates = relativeCoordinatesFromMouseEvent(event); + if (maybeCoordinates !== null) { + dispatch({ + type: 'userStartedPanning', + payload: { screenCoordinates: maybeCoordinates, time: sideEffectors.timestamp() }, + }); + } + }, + [dispatch, relativeCoordinatesFromMouseEvent, sideEffectors] + ); + + const handleMouseMove = useCallback( + (event: MouseEvent) => { + const maybeCoordinates = relativeCoordinatesFromMouseEvent(event); + if (maybeCoordinates) { + dispatch({ + type: 'userMovedPointer', + payload: { + screenCoordinates: maybeCoordinates, + time: sideEffectors.timestamp(), + }, + }); + } + }, + [dispatch, relativeCoordinatesFromMouseEvent, sideEffectors] + ); + + const handleMouseUp = useCallback(() => { + if (userIsPanning) { + dispatch({ + type: 'userStoppedPanning', + payload: { + time: sideEffectors.timestamp(), + }, + }); + } + }, [dispatch, sideEffectors, userIsPanning]); + + const handleWheel = useCallback( + (event: WheelEvent) => { + if ( + elementBoundingClientRect !== null && + event.ctrlKey && + event.deltaY !== 0 && + event.deltaMode === 0 + ) { + event.preventDefault(); + dispatch({ + type: 'userZoomed', + payload: { + /** + * we use elementBoundingClientRect to interpret pixel deltas as a fraction of the element's height + * when pinch-zooming in on a mac, deltaY is a negative number but we want the payload to be positive + */ + zoomChange: event.deltaY / -elementBoundingClientRect.height, + time: sideEffectors.timestamp(), + }, + }); + } + }, + [elementBoundingClientRect, dispatch, sideEffectors] + ); + + const refCallback = useCallback( + (node: null | HTMLDivElement) => { + setRef(node); + clientRectCallback(node); + }, + [clientRectCallback] + ); + + useEffect(() => { + window.addEventListener('mouseup', handleMouseUp, { passive: true }); + return () => { + window.removeEventListener('mouseup', handleMouseUp); + }; + }, [handleMouseUp]); + + useEffect(() => { + window.addEventListener('mousemove', handleMouseMove, { passive: true }); + return () => { + window.removeEventListener('mousemove', handleMouseMove); + }; + }, [handleMouseMove]); + + /** + * Register an event handler directly on `elementRef` for the `wheel` event, with no options + * React sets native event listeners on the `window` and calls provided handlers via event propagation. + * As of Chrome 73, `'wheel'` events on `window` are automatically treated as 'passive'. + * If you don't need to call `event.preventDefault` then you should use regular React event handling instead. + */ + useEffect(() => { + if (ref !== null) { + ref.addEventListener('wheel', handleWheel); + return () => { + ref.removeEventListener('wheel', handleWheel); + }; + } + }, [ref, handleWheel]); + + /** + * Allow rAF loop to indirectly read projectionMatrixAtTime via a ref. Since it also + * sets projectionMatrixAtTime, relying directly on it causes considerable jank. + */ + useLayoutEffect(() => { + projectionMatrixAtTimeRef.current = projectionMatrixAtTime; + }, [projectionMatrixAtTime]); + + /** + * Keep the projection matrix state in sync with the selector. + * This isn't needed during animation. + */ + useLayoutEffect(() => { + // Update the projection matrix that we return, rerendering any component that uses this. + setProjectionMatrix(projectionMatrixAtTime(sideEffectors.timestamp())); + }, [projectionMatrixAtTime, sideEffectors]); + + /** + * When animation is happening, run a rAF loop, when it is done, stop. + */ + useLayoutEffect( + () => { + const startDate = sideEffectors.timestamp(); + if (isAnimatingAtTime(startDate)) { + let rafRef: null | number = null; + const handleFrame = () => { + // Get the current timestamp, now that the frame is ready + const date = sideEffectors.timestamp(); + if (projectionMatrixAtTimeRef.current !== undefined) { + // Update the projection matrix, triggering a rerender + setProjectionMatrix(projectionMatrixAtTimeRef.current(date)); + } + // If we are still animating, request another frame, continuing the loop + if (isAnimatingAtTime(date)) { + rafRef = sideEffectors.requestAnimationFrame(handleFrame); + } else { + /** + * `isAnimatingAtTime` was false, meaning that the animation is complete. + * Do not request another animation frame. + */ + rafRef = null; + } + }; + // Kick off the loop by requestion an animation frame + rafRef = sideEffectors.requestAnimationFrame(handleFrame); + + /** + * This function cancels the animation frame request. The cancel function + * will occur when the component is unmounted. It will also occur when a dependency + * changes. + * + * The `isAnimatingAtTime` dependency will be changed if the animation state changes. The animation + * state only changes when the user animates again (e.g. brings a different node into view, or nudges the + * camera.) + */ + return () => { + // Cancel the animation frame. + if (rafRef !== null) { + sideEffectors.cancelAnimationFrame(rafRef); + } + }; + } + }, + /** + * `isAnimatingAtTime` is a function created with `reselect`. The function reference will be changed when + * the animation state changes. When the function reference has changed, you *might* be animating. + */ + [isAnimatingAtTime, sideEffectors] + ); + + useEffect(() => { + if (elementBoundingClientRect !== null) { + dispatch({ + type: 'userSetRasterSize', + payload: [elementBoundingClientRect.width, elementBoundingClientRect.height], + }); + } + }, [dispatch, elementBoundingClientRect]); + + return { + ref: refCallback, + onMouseDown: handleMouseDown, + projectionMatrix, + }; +} + +/** + * Returns a nullable DOMRect and a ref callback. Pass the refCallback to the + * `ref` property of a native element and this hook will return a DOMRect for + * it by calling `getBoundingClientRect`. This hook will observe the element + * with a resize observer and call getBoundingClientRect again after resizes. + * + * Note that the changes to the position of the element aren't automatically + * tracked. So if the element's position moves for some reason, be sure to + * handle that. + */ +function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | null) => void] { + const [rect, setRect] = useState<DOMRect | null>(null); + const nodeRef = useRef<Element | null>(null); + const ref = useCallback((node: Element | null) => { + nodeRef.current = node; + if (node !== null) { + setRect(node.getBoundingClientRect()); + } + }, []); + const { ResizeObserver } = useContext(SideEffectContext); + useEffect(() => { + if (nodeRef.current !== null) { + const resizeObserver = new ResizeObserver(entries => { + if (nodeRef.current !== null && nodeRef.current === entries[0].target) { + setRect(nodeRef.current.getBoundingClientRect()); + } + }); + resizeObserver.observe(nodeRef.current); + return () => { + resizeObserver.disconnect(); + }; + } + }, [ResizeObserver, nodeRef]); + return [rect, ref]; +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_nonpassive_wheel_handler.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_nonpassive_wheel_handler.tsx deleted file mode 100644 index a0738bcf4d14c6..00000000000000 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_nonpassive_wheel_handler.tsx +++ /dev/null @@ -1,26 +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 { useEffect } from 'react'; -/** - * Register an event handler directly on `elementRef` for the `wheel` event, with no options - * React sets native event listeners on the `window` and calls provided handlers via event propagation. - * As of Chrome 73, `'wheel'` events on `window` are automatically treated as 'passive'. - * If you don't need to call `event.preventDefault` then you should use regular React event handling instead. - */ -export function useNonPassiveWheelHandler( - handler: (event: WheelEvent) => void, - elementRef: HTMLElement | null -) { - useEffect(() => { - if (elementRef !== null) { - elementRef.addEventListener('wheel', handler); - return () => { - elementRef.removeEventListener('wheel', handler); - }; - } - }, [elementRef, handler]); -} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_resolver_dispatch.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_resolver_dispatch.ts new file mode 100644 index 00000000000000..a993a4ed595e1b --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_resolver_dispatch.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useDispatch } from 'react-redux'; +import { ResolverAction } from '../types'; + +/** + * Call `useDispatch`, but only accept `ResolverAction` actions. + */ +export const useResolverDispatch: () => (action: ResolverAction) => unknown = useDispatch; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6bcf61b53fd5fa..79b826bc4524fe 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -78,109 +78,6 @@ "messages": { "common.ui.aggResponse.allDocsTitle": "すべてのドキュメント", "common.ui.aggTypes.rangesFormatMessage": "{gte} {from} と {lt} {to}", - "data.search.aggs.aggGroups.bucketsText": "バケット", - "data.search.aggs.aggGroups.metricsText": "メトリック", - "data.search.aggs.buckets.dateHistogramLabel": "{intervalDescription}ごとの {fieldName}", - "data.search.aggs.buckets.dateHistogramTitle": "日付ヒストグラム", - "data.search.aggs.buckets.dateRangeTitle": "日付範囲", - "data.search.aggs.buckets.filtersTitle": "フィルター", - "data.search.aggs.buckets.filterTitle": "フィルター", - "data.search.aggs.buckets.geohashGridTitle": "ジオハッシュ", - "data.search.aggs.buckets.geotileGridTitle": "ジオタイル", - "data.search.aggs.buckets.histogramTitle": "ヒストグラム", - "data.search.aggs.buckets.intervalOptions.autoDisplayName": "自動", - "data.search.aggs.buckets.intervalOptions.dailyDisplayName": "日ごと", - "data.search.aggs.buckets.intervalOptions.hourlyDisplayName": "1 時間ごと", - "data.search.aggs.buckets.intervalOptions.millisecondDisplayName": "ミリ秒", - "data.search.aggs.buckets.intervalOptions.minuteDisplayName": "分", - "data.search.aggs.buckets.intervalOptions.monthlyDisplayName": "月ごと", - "data.search.aggs.buckets.intervalOptions.secondDisplayName": "秒", - "data.search.aggs.buckets.intervalOptions.weeklyDisplayName": "週ごと", - "data.search.aggs.buckets.intervalOptions.yearlyDisplayName": "1 年ごと", - "data.search.aggs.buckets.ipRangeLabel": "{fieldName} IP 範囲", - "data.search.aggs.buckets.ipRangeTitle": "IPv4 範囲", - "data.search.aggs.aggTypes.rangesFormatMessage": "{gte} {from} と {lt} {to}", - "data.search.aggs.aggTypesLabel": "{fieldName} の範囲", - "data.search.aggs.buckets.rangeTitle": "範囲", - "data.search.aggs.buckets.significantTerms.excludeLabel": "除外", - "data.search.aggs.buckets.significantTerms.includeLabel": "含める", - "data.search.aggs.buckets.significantTermsLabel": "{fieldName} のトップ {size} の珍しいアイテム", - "data.search.aggs.buckets.significantTermsTitle": "重要な用語", - "data.search.aggs.buckets.terms.excludeLabel": "除外", - "data.search.aggs.buckets.terms.includeLabel": "含める", - "data.search.aggs.buckets.terms.missingBucketLabel": "欠測値", - "data.search.aggs.buckets.terms.orderAscendingTitle": "昇順", - "data.search.aggs.buckets.terms.orderDescendingTitle": "降順", - "data.search.aggs.buckets.terms.otherBucketDescription": "このリクエストは、データバケットの基準外のドキュメントの数をカウントします。", - "data.search.aggs.buckets.terms.otherBucketLabel": "その他", - "data.search.aggs.buckets.terms.otherBucketTitle": "他のバケット", - "data.search.aggs.buckets.termsTitle": "用語", - "data.search.aggs.histogram.missingMaxMinValuesWarning": "自動スケールヒストグラムバケットから最高値と最低値を取得できません。これによりビジュアライゼーションのパフォーマンスが低下する可能性があります。", - "data.search.aggs.metrics.averageBucketTitle": "平均バケット", - "data.search.aggs.metrics.averageLabel": "平均 {field}", - "data.search.aggs.metrics.averageTitle": "平均", - "data.search.aggs.metrics.bucketAggTitle": "バケット集約", - "data.search.aggs.metrics.countLabel": "カウント", - "data.search.aggs.metrics.countTitle": "カウント", - "data.search.aggs.metrics.cumulativeSumLabel": "累積合計", - "data.search.aggs.metrics.cumulativeSumTitle": "累積合計", - "data.search.aggs.metrics.derivativeLabel": "派生", - "data.search.aggs.metrics.derivativeTitle": "派生", - "data.search.aggs.metrics.geoBoundsLabel": "境界", - "data.search.aggs.metrics.geoBoundsTitle": "境界", - "data.search.aggs.metrics.geoCentroidLabel": "ジオセントロイド", - "data.search.aggs.metrics.geoCentroidTitle": "ジオセントロイド", - "data.search.aggs.metrics.maxBucketTitle": "最高バケット", - "data.search.aggs.metrics.maxLabel": "最高 {field}", - "data.search.aggs.metrics.maxTitle": "最高", - "data.search.aggs.metrics.medianLabel": "中央 {field}", - "data.search.aggs.metrics.medianTitle": "中央", - "data.search.aggs.metrics.metricAggregationsSubtypeTitle": "メトリック集約", - "data.search.aggs.metrics.metricAggTitle": "メトリック集約", - "data.search.aggs.metrics.minBucketTitle": "最低バケット", - "data.search.aggs.metrics.minLabel": "最低 {field}", - "data.search.aggs.metrics.minTitle": "最低", - "data.search.aggs.metrics.movingAvgLabel": "移動平均", - "data.search.aggs.metrics.movingAvgTitle": "移動平均", - "data.search.aggs.metrics.overallAverageLabel": "全体平均", - "data.search.aggs.metrics.overallMaxLabel": "全体最高", - "data.search.aggs.metrics.overallMinLabel": "全体最低", - "data.search.aggs.metrics.overallSumLabel": "全体合計", - "data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle": "親パイプライン集約", - "data.search.aggs.metrics.percentileRanks.valuePropsLabel": "「{label}」の {format} のパーセンタイル順位", - "data.search.aggs.metrics.percentileRanksLabel": "{field} のパーセンタイル順位", - "data.search.aggs.metrics.percentileRanksTitle": "パーセンタイル順位", - "data.search.aggs.metrics.percentiles.valuePropsLabel": "{label} の {percentile} パーセンタイル", - "data.search.aggs.metrics.percentilesLabel": "{field} のパーセンタイル", - "data.search.aggs.metrics.percentilesTitle": "パーセンタイル", - "data.search.aggs.metrics.serialDiffLabel": "差分の推移", - "data.search.aggs.metrics.serialDiffTitle": "差分の推移", - "data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle": "シブリングパイプラインアグリゲーション", - "data.search.aggs.metrics.standardDeviation.keyDetailsLabel": "{fieldDisplayName} の標準偏差", - "data.search.aggs.metrics.standardDeviation.lowerKeyDetailsTitle": "下の{label}", - "data.search.aggs.metrics.standardDeviation.upperKeyDetailsTitle": "上の{label}", - "data.search.aggs.metrics.standardDeviationLabel": "{field} の標準偏差", - "data.search.aggs.metrics.standardDeviationTitle": "標準偏差", - "data.search.aggs.metrics.sumBucketTitle": "合計バケット", - "data.search.aggs.metrics.sumLabel": "{field} の合計", - "data.search.aggs.metrics.sumTitle": "合計", - "data.search.aggs.metrics.topHit.ascendingLabel": "昇順", - "data.search.aggs.metrics.topHit.averageLabel": "平均", - "data.search.aggs.metrics.topHit.concatenateLabel": "連結", - "data.search.aggs.metrics.topHit.descendingLabel": "降順", - "data.search.aggs.metrics.topHit.firstPrefixLabel": "最初", - "data.search.aggs.metrics.topHit.lastPrefixLabel": "最後", - "data.search.aggs.metrics.topHit.maxLabel": "最高", - "data.search.aggs.metrics.topHit.minLabel": "最低", - "data.search.aggs.metrics.topHit.sumLabel": "合計", - "data.search.aggs.metrics.topHitTitle": "トップヒット", - "data.search.aggs.metrics.uniqueCountLabel": "{field} のユニークカウント", - "data.search.aggs.metrics.uniqueCountTitle": "ユニークカウント", - "data.search.aggs.otherBucket.labelForMissingValuesLabel": "欠測値のラベル", - "data.search.aggs.otherBucket.labelForOtherBucketLabel": "他のバケットのラベル", - "data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "保存された {fieldParameter} パラメーターが無効になりました。新しいフィールドを選択してください。", - "data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} は必須パラメーターです", - "data.search.aggs.string.customLabel": "カスタムラベル", "common.ui.directives.paginate.size.allDropDownOptionLabel": "すべて", "common.ui.dualRangeControl.mustSetBothErrorMessage": "下と上の値の両方を設定する必要があります", "common.ui.dualRangeControl.outsideOfRangeErrorMessage": "値は {min} と {max} の間でなければなりません", @@ -367,10 +264,223 @@ "common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", "common.ui.url.replacementFailedErrorMessage": "置換に失敗、未解決の表現式: {expr}", "common.ui.url.savedObjectIsMissingNotificationMessage": "保存されたオブジェクトがありません", - "data.search.aggs.percentageOfLabel": "{label} のパーセンテージ", "common.ui.vis.defaultFeedbackMessage": "フィードバックがありますか?{link} で問題を報告してください。", "common.ui.vis.kibanaMap.leaflet.fitDataBoundsAriaLabel": "データバウンドを合わせる", "common.ui.vis.kibanaMap.zoomWarning": "ズームレベルが最大に達しました。完全にズームインするには、Elasticsearch と Kibana の {defaultDistribution} にアップグレードしてください。{ems} でより多くのズームレベルが利用できます。または、独自のマップサーバーを構成できます。詳細は { wms } または { configSettings} をご覧ください。", + "data.search.aggs.aggGroups.bucketsText": "バケット", + "data.search.aggs.aggGroups.metricsText": "メトリック", + "data.search.aggs.buckets.dateHistogramLabel": "{intervalDescription}ごとの {fieldName}", + "data.search.aggs.buckets.dateHistogramTitle": "日付ヒストグラム", + "data.search.aggs.buckets.dateRangeTitle": "日付範囲", + "data.search.aggs.buckets.filtersTitle": "フィルター", + "data.search.aggs.buckets.filterTitle": "フィルター", + "data.search.aggs.buckets.geohashGridTitle": "ジオハッシュ", + "data.search.aggs.buckets.geotileGridTitle": "ジオタイル", + "data.search.aggs.buckets.histogramTitle": "ヒストグラム", + "data.search.aggs.buckets.intervalOptions.autoDisplayName": "自動", + "data.search.aggs.buckets.intervalOptions.dailyDisplayName": "日ごと", + "data.search.aggs.buckets.intervalOptions.hourlyDisplayName": "1 時間ごと", + "data.search.aggs.buckets.intervalOptions.millisecondDisplayName": "ミリ秒", + "data.search.aggs.buckets.intervalOptions.minuteDisplayName": "分", + "data.search.aggs.buckets.intervalOptions.monthlyDisplayName": "月ごと", + "data.search.aggs.buckets.intervalOptions.secondDisplayName": "秒", + "data.search.aggs.buckets.intervalOptions.weeklyDisplayName": "週ごと", + "data.search.aggs.buckets.intervalOptions.yearlyDisplayName": "1 年ごと", + "data.search.aggs.buckets.ipRangeLabel": "{fieldName} IP 範囲", + "data.search.aggs.buckets.ipRangeTitle": "IPv4 範囲", + "data.search.aggs.aggTypes.rangesFormatMessage": "{gte} {from} と {lt} {to}", + "data.search.aggs.aggTypesLabel": "{fieldName} の範囲", + "data.search.aggs.buckets.rangeTitle": "範囲", + "data.search.aggs.buckets.significantTerms.excludeLabel": "除外", + "data.search.aggs.buckets.significantTerms.includeLabel": "含める", + "data.search.aggs.buckets.significantTermsLabel": "{fieldName} のトップ {size} の珍しいアイテム", + "data.search.aggs.buckets.significantTermsTitle": "重要な用語", + "data.search.aggs.buckets.terms.excludeLabel": "除外", + "data.search.aggs.buckets.terms.includeLabel": "含める", + "data.search.aggs.buckets.terms.missingBucketLabel": "欠測値", + "data.search.aggs.buckets.terms.orderAscendingTitle": "昇順", + "data.search.aggs.buckets.terms.orderDescendingTitle": "降順", + "data.search.aggs.buckets.terms.otherBucketDescription": "このリクエストは、データバケットの基準外のドキュメントの数をカウントします。", + "data.search.aggs.buckets.terms.otherBucketLabel": "その他", + "data.search.aggs.buckets.terms.otherBucketTitle": "他のバケット", + "data.search.aggs.buckets.termsTitle": "用語", + "data.search.aggs.histogram.missingMaxMinValuesWarning": "自動スケールヒストグラムバケットから最高値と最低値を取得できません。これによりビジュアライゼーションのパフォーマンスが低下する可能性があります。", + "data.search.aggs.metrics.averageBucketTitle": "平均バケット", + "data.search.aggs.metrics.averageLabel": "平均 {field}", + "data.search.aggs.metrics.averageTitle": "平均", + "data.search.aggs.metrics.bucketAggTitle": "バケット集約", + "data.search.aggs.metrics.countLabel": "カウント", + "data.search.aggs.metrics.countTitle": "カウント", + "data.search.aggs.metrics.cumulativeSumLabel": "累積合計", + "data.search.aggs.metrics.cumulativeSumTitle": "累積合計", + "data.search.aggs.metrics.derivativeLabel": "派生", + "data.search.aggs.metrics.derivativeTitle": "派生", + "data.search.aggs.metrics.geoBoundsLabel": "境界", + "data.search.aggs.metrics.geoBoundsTitle": "境界", + "data.search.aggs.metrics.geoCentroidLabel": "ジオセントロイド", + "data.search.aggs.metrics.geoCentroidTitle": "ジオセントロイド", + "data.search.aggs.metrics.maxBucketTitle": "最高バケット", + "data.search.aggs.metrics.maxLabel": "最高 {field}", + "data.search.aggs.metrics.maxTitle": "最高", + "data.search.aggs.metrics.medianLabel": "中央 {field}", + "data.search.aggs.metrics.medianTitle": "中央", + "data.search.aggs.metrics.metricAggregationsSubtypeTitle": "メトリック集約", + "data.search.aggs.metrics.metricAggTitle": "メトリック集約", + "data.search.aggs.metrics.minBucketTitle": "最低バケット", + "data.search.aggs.metrics.minLabel": "最低 {field}", + "data.search.aggs.metrics.minTitle": "最低", + "data.search.aggs.metrics.movingAvgLabel": "移動平均", + "data.search.aggs.metrics.movingAvgTitle": "移動平均", + "data.search.aggs.metrics.overallAverageLabel": "全体平均", + "data.search.aggs.metrics.overallMaxLabel": "全体最高", + "data.search.aggs.metrics.overallMinLabel": "全体最低", + "data.search.aggs.metrics.overallSumLabel": "全体合計", + "data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle": "親パイプライン集約", + "data.search.aggs.metrics.percentileRanks.valuePropsLabel": "「{label}」の {format} のパーセンタイル順位", + "data.search.aggs.metrics.percentileRanksLabel": "{field} のパーセンタイル順位", + "data.search.aggs.metrics.percentileRanksTitle": "パーセンタイル順位", + "data.search.aggs.metrics.percentiles.valuePropsLabel": "{label} の {percentile} パーセンタイル", + "data.search.aggs.metrics.percentilesLabel": "{field} のパーセンタイル", + "data.search.aggs.metrics.percentilesTitle": "パーセンタイル", + "data.search.aggs.metrics.serialDiffLabel": "差分の推移", + "data.search.aggs.metrics.serialDiffTitle": "差分の推移", + "data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle": "シブリングパイプラインアグリゲーション", + "data.search.aggs.metrics.standardDeviation.keyDetailsLabel": "{fieldDisplayName} の標準偏差", + "data.search.aggs.metrics.standardDeviation.lowerKeyDetailsTitle": "下の{label}", + "data.search.aggs.metrics.standardDeviation.upperKeyDetailsTitle": "上の{label}", + "data.search.aggs.metrics.standardDeviationLabel": "{field} の標準偏差", + "data.search.aggs.metrics.standardDeviationTitle": "標準偏差", + "data.search.aggs.metrics.sumBucketTitle": "合計バケット", + "data.search.aggs.metrics.sumLabel": "{field} の合計", + "data.search.aggs.metrics.sumTitle": "合計", + "data.search.aggs.metrics.topHit.ascendingLabel": "昇順", + "data.search.aggs.metrics.topHit.averageLabel": "平均", + "data.search.aggs.metrics.topHit.concatenateLabel": "連結", + "data.search.aggs.metrics.topHit.descendingLabel": "降順", + "data.search.aggs.metrics.topHit.firstPrefixLabel": "最初", + "data.search.aggs.metrics.topHit.lastPrefixLabel": "最後", + "data.search.aggs.metrics.topHit.maxLabel": "最高", + "data.search.aggs.metrics.topHit.minLabel": "最低", + "data.search.aggs.metrics.topHit.sumLabel": "合計", + "data.search.aggs.metrics.topHitTitle": "トップヒット", + "data.search.aggs.metrics.uniqueCountLabel": "{field} のユニークカウント", + "data.search.aggs.metrics.uniqueCountTitle": "ユニークカウント", + "data.search.aggs.otherBucket.labelForMissingValuesLabel": "欠測値のラベル", + "data.search.aggs.otherBucket.labelForOtherBucketLabel": "他のバケットのラベル", + "data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "保存された {fieldParameter} パラメーターが無効になりました。新しいフィールドを選択してください。", + "data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} は必須パラメーターです", + "data.search.aggs.string.customLabel": "カスタムラベル", + "data.search.aggs.percentageOfLabel": "{label} のパーセンテージ", + "data.filter.applyFilters.popupHeader": "適用するフィルターの選択", + "data.filter.applyFiltersPopup.cancelButtonLabel": "キャンセル", + "data.filter.applyFiltersPopup.saveButtonLabel": "適用", + "data.filter.filterBar.addFilterButtonLabel": "フィルターを追加します", + "data.filter.filterBar.deleteFilterButtonLabel": "削除", + "data.filter.filterBar.disabledFilterPrefix": "無効", + "data.filter.filterBar.disableFilterButtonLabel": "一時的に無効にする", + "data.filter.filterBar.editFilterButtonLabel": "フィルターを編集", + "data.filter.filterBar.enableFilterButtonLabel": "再度有効にする", + "data.filter.filterBar.excludeFilterButtonLabel": "結果を除外", + "data.filter.filterBar.filterItemBadgeAriaLabel": "フィルターアクション", + "data.filter.filterBar.filterItemBadgeIconAriaLabel": "削除", + "data.filter.filterBar.includeFilterButtonLabel": "結果を含める", + "data.filter.filterBar.indexPatternSelectPlaceholder": "インデックスパターンの選択", + "data.filter.filterBar.moreFilterActionsMessage": "フィルター:{innerText}。他のフィルターアクションを使用するには選択してください。", + "data.filter.filterBar.negatedFilterPrefix": "NOT ", + "data.filter.filterBar.pinFilterButtonLabel": "すべてのアプリにピン付け", + "data.filter.filterBar.pinnedFilterPrefix": "ピン付け済み", + "data.filter.filterBar.unpinFilterButtonLabel": "ピンを外す", + "data.filter.filterEditor.cancelButtonLabel": "キャンセル", + "data.filter.filterEditor.createCustomLabelInputLabel": "カスタムラベル", + "data.filter.filterEditor.createCustomLabelSwitchLabel": "カスタムラベルを作成しますか?", + "data.filter.filterEditor.dateFormatHelpLinkLabel": "対応データフォーマット", + "data.filter.filterEditor.doesNotExistOperatorOptionLabel": "存在しません", + "data.filter.filterEditor.editFilterPopupTitle": "フィルターを編集", + "data.filter.filterEditor.editFilterValuesButtonLabel": "フィルター値を編集", + "data.filter.filterEditor.editQueryDslButtonLabel": "クエリ DSL として編集", + "data.filter.filterEditor.existsOperatorOptionLabel": "存在する", + "data.filter.filterEditor.falseOptionLabel": "False", + "data.filter.filterEditor.fieldSelectLabel": "フィールド", + "data.filter.filterEditor.fieldSelectPlaceholder": "フィールドを選択", + "data.filter.filterEditor.indexPatternSelectLabel": "インデックスパターン", + "data.filter.filterEditor.isBetweenOperatorOptionLabel": "is between", + "data.filter.filterEditor.isNotBetweenOperatorOptionLabel": "is not between", + "data.filter.filterEditor.isNotOneOfOperatorOptionLabel": "is not one of", + "data.filter.filterEditor.isNotOperatorOptionLabel": "is not", + "data.filter.filterEditor.isOneOfOperatorOptionLabel": "is one of", + "data.filter.filterEditor.isOperatorOptionLabel": "が", + "data.filter.filterEditor.operatorSelectLabel": "演算子", + "data.filter.filterEditor.operatorSelectPlaceholderSelect": "選択してください", + "data.filter.filterEditor.operatorSelectPlaceholderWaiting": "待機中", + "data.filter.filterEditor.queryDslLabel": "Elasticsearch クエリ DSL", + "data.filter.filterEditor.rangeEndInputPlaceholder": "範囲の終了値", + "data.filter.filterEditor.rangeInputLabel": "範囲", + "data.filter.filterEditor.rangeStartInputPlaceholder": "範囲の開始値", + "data.filter.filterEditor.saveButtonLabel": "保存", + "data.filter.filterEditor.trueOptionLabel": "True", + "data.filter.filterEditor.valueInputLabel": "値", + "data.filter.filterEditor.valueInputPlaceholder": "値を入力", + "data.filter.filterEditor.valueSelectPlaceholder": "値を選択", + "data.filter.filterEditor.valuesSelectLabel": "値", + "data.filter.filterEditor.valuesSelectPlaceholder": "値を選択", + "data.filter.options.changeAllFiltersButtonLabel": "すべてのフィルターの変更", + "data.filter.options.deleteAllFiltersButtonLabel": "すべて削除", + "data.filter.options.disableAllFiltersButtonLabel": "すべて無効にする", + "data.filter.options.enableAllFiltersButtonLabel": "すべて有効にする", + "data.filter.options.invertDisabledFiltersButtonLabel": "有効・無効を反転", + "data.filter.options.invertNegatedFiltersButtonLabel": "含める・除外を反転", + "data.filter.options.pinAllFiltersButtonLabel": "すべてピン付け", + "data.filter.options.unpinAllFiltersButtonLabel": "すべてのピンを外す", + "data.filter.searchBar.changeAllFiltersTitle": "すべてのフィルターの変更", + "data.indexPatterns.unableWriteLabel": "インデックスパターンを書き込めません!このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", + "data.indexPatterns.unknownFieldErrorMessage": "インデックスパターン「{title}」のフィールド「{name}」が不明なフィールドタイプを使用しています。", + "data.indexPatterns.unknownFieldHeader": "不明なフィールドタイプ {type}", + "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "無効なカレンダー間隔:{interval}、1よりも大きな値が必要です", + "data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "無効な間隔フォーマット:{interval}", + "data.query.queryBar.comboboxAriaLabel": "{pageType} ページの検索とフィルタリング", + "data.query.queryBar.kqlFullLanguageName": "Kibana クエリ言語", + "data.query.queryBar.kqlLanguageName": "KQL", + "data.query.queryBar.kqlOffLabel": "オフ", + "data.query.queryBar.kqlOnLabel": "オン", + "data.query.queryBar.luceneLanguageName": "Lucene", + "data.query.queryBar.luceneSyntaxWarningMessage": "Lucene クエリ構文を使用しているようですが、Kibana クエリ言語 (KQL) が選択されています。KQL ドキュメント {link} を確認してください。", + "data.query.queryBar.luceneSyntaxWarningOptOutText": "今後表示しない", + "data.query.queryBar.luceneSyntaxWarningTitle": "Lucene 構文警告", + "data.query.queryBar.searchInputAriaLabel": "{pageType} ページの検索とフィルタリングを行うには入力を開始してください", + "data.query.queryBar.searchInputPlaceholder": "検索", + "data.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) は、シンプルなクエリ構文とスクリプトフィールドのサポートを提供します。また、KQL はベーシックライセンス以上をご利用の場合、自動入力も提供します。KQL をオフにすると、Kibana は Lucene を使用します。", + "data.query.queryBar.syntaxOptionsDescription.docsLinkText": "こちら", + "data.query.queryBar.syntaxOptionsTitle": "構文オプション", + "data.search.searchBar.savedQueryDescriptionLabelText": "説明", + "data.search.searchBar.savedQueryDescriptionText": "再度使用するクエリテキストとフィルターを保存します。", + "data.search.searchBar.savedQueryForm.titleConflictText": "タイトルが既に保存されているクエリに使用されています", + "data.search.searchBar.savedQueryForm.titleMissingText": "名前が必要です", + "data.search.searchBar.savedQueryForm.whitespaceErrorText": "タイトルの始めと終わりにはスペースを使用できません", + "data.search.searchBar.savedQueryFormCancelButtonText": "キャンセル", + "data.search.searchBar.savedQueryFormSaveButtonText": "保存", + "data.search.searchBar.savedQueryFormTitle": "クエリを保存", + "data.search.searchBar.savedQueryIncludeFiltersLabelText": "フィルターを含める", + "data.search.searchBar.savedQueryIncludeTimeFilterLabelText": "時間フィルターを含める", + "data.search.searchBar.savedQueryNameHelpText": "名前が必要です。タイトルの始めと終わりにはスペースを使用できません。名前は固有でなければなりません。", + "data.search.searchBar.savedQueryNameLabelText": "名前", + "data.search.searchBar.savedQueryNoSavedQueriesText": "保存されたクエリがありません。", + "data.search.searchBar.savedQueryPopoverButtonText": "保存されたクエリを表示", + "data.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "現在保存されているクエリを消去", + "data.search.searchBar.savedQueryPopoverClearButtonText": "消去", + "data.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "キャンセル", + "data.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "削除", + "data.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "「{savedQueryName}」を削除しますか?", + "data.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "保存されたクエリ {savedQueryName} を削除", + "data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "新規保存クエリを保存", + "data.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "新規保存", + "data.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "新規保存クエリを保存", + "data.search.searchBar.savedQueryPopoverSaveButtonText": "現在のクエリを保存", + "data.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "{title} への変更を保存", + "data.search.searchBar.savedQueryPopoverSaveChangesButtonText": "変更を保存", + "data.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "保存クエリボタン {savedQueryName}", + "data.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName} の説明", + "data.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "選択されたクエリボタン {savedQueryName} を保存しました。変更を破棄するには押してください。", + "data.search.searchBar.savedQueryPopoverTitleText": "保存されたクエリ", "charts.colormaps.bluesText": "青", "charts.colormaps.greensText": "緑", "charts.colormaps.greenToRedText": "緑から赤", @@ -526,116 +636,6 @@ "dashboardEmbeddableContainer.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "全画面", "dashboardEmbeddableContainer.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "ダッシュボードが読み込めません。", "dashboardEmbeddableContainer.factory.displayName": "ダッシュボード", - "data.filter.applyFilters.popupHeader": "適用するフィルターの選択", - "data.filter.applyFiltersPopup.cancelButtonLabel": "キャンセル", - "data.filter.applyFiltersPopup.saveButtonLabel": "適用", - "data.filter.filterBar.addFilterButtonLabel": "フィルターを追加します", - "data.filter.filterBar.deleteFilterButtonLabel": "削除", - "data.filter.filterBar.disabledFilterPrefix": "無効", - "data.filter.filterBar.disableFilterButtonLabel": "一時的に無効にする", - "data.filter.filterBar.editFilterButtonLabel": "フィルターを編集", - "data.filter.filterBar.enableFilterButtonLabel": "再度有効にする", - "data.filter.filterBar.excludeFilterButtonLabel": "結果を除外", - "data.filter.filterBar.filterItemBadgeAriaLabel": "フィルターアクション", - "data.filter.filterBar.filterItemBadgeIconAriaLabel": "削除", - "data.filter.filterBar.includeFilterButtonLabel": "結果を含める", - "data.filter.filterBar.indexPatternSelectPlaceholder": "インデックスパターンの選択", - "data.filter.filterBar.moreFilterActionsMessage": "フィルター:{innerText}。他のフィルターアクションを使用するには選択してください。", - "data.filter.filterBar.negatedFilterPrefix": "NOT ", - "data.filter.filterBar.pinFilterButtonLabel": "すべてのアプリにピン付け", - "data.filter.filterBar.pinnedFilterPrefix": "ピン付け済み", - "data.filter.filterBar.unpinFilterButtonLabel": "ピンを外す", - "data.filter.filterEditor.cancelButtonLabel": "キャンセル", - "data.filter.filterEditor.createCustomLabelInputLabel": "カスタムラベル", - "data.filter.filterEditor.createCustomLabelSwitchLabel": "カスタムラベルを作成しますか?", - "data.filter.filterEditor.dateFormatHelpLinkLabel": "対応データフォーマット", - "data.filter.filterEditor.doesNotExistOperatorOptionLabel": "存在しません", - "data.filter.filterEditor.editFilterPopupTitle": "フィルターを編集", - "data.filter.filterEditor.editFilterValuesButtonLabel": "フィルター値を編集", - "data.filter.filterEditor.editQueryDslButtonLabel": "クエリ DSL として編集", - "data.filter.filterEditor.existsOperatorOptionLabel": "存在する", - "data.filter.filterEditor.falseOptionLabel": "False", - "data.filter.filterEditor.fieldSelectLabel": "フィールド", - "data.filter.filterEditor.fieldSelectPlaceholder": "フィールドを選択", - "data.filter.filterEditor.indexPatternSelectLabel": "インデックスパターン", - "data.filter.filterEditor.isBetweenOperatorOptionLabel": "is between", - "data.filter.filterEditor.isNotBetweenOperatorOptionLabel": "is not between", - "data.filter.filterEditor.isNotOneOfOperatorOptionLabel": "is not one of", - "data.filter.filterEditor.isNotOperatorOptionLabel": "is not", - "data.filter.filterEditor.isOneOfOperatorOptionLabel": "is one of", - "data.filter.filterEditor.isOperatorOptionLabel": "が", - "data.filter.filterEditor.operatorSelectLabel": "演算子", - "data.filter.filterEditor.operatorSelectPlaceholderSelect": "選択してください", - "data.filter.filterEditor.operatorSelectPlaceholderWaiting": "待機中", - "data.filter.filterEditor.queryDslLabel": "Elasticsearch クエリ DSL", - "data.filter.filterEditor.rangeEndInputPlaceholder": "範囲の終了値", - "data.filter.filterEditor.rangeInputLabel": "範囲", - "data.filter.filterEditor.rangeStartInputPlaceholder": "範囲の開始値", - "data.filter.filterEditor.saveButtonLabel": "保存", - "data.filter.filterEditor.trueOptionLabel": "True", - "data.filter.filterEditor.valueInputLabel": "値", - "data.filter.filterEditor.valueInputPlaceholder": "値を入力", - "data.filter.filterEditor.valueSelectPlaceholder": "値を選択", - "data.filter.filterEditor.valuesSelectLabel": "値", - "data.filter.filterEditor.valuesSelectPlaceholder": "値を選択", - "data.filter.options.changeAllFiltersButtonLabel": "すべてのフィルターの変更", - "data.filter.options.deleteAllFiltersButtonLabel": "すべて削除", - "data.filter.options.disableAllFiltersButtonLabel": "すべて無効にする", - "data.filter.options.enableAllFiltersButtonLabel": "すべて有効にする", - "data.filter.options.invertDisabledFiltersButtonLabel": "有効・無効を反転", - "data.filter.options.invertNegatedFiltersButtonLabel": "含める・除外を反転", - "data.filter.options.pinAllFiltersButtonLabel": "すべてピン付け", - "data.filter.options.unpinAllFiltersButtonLabel": "すべてのピンを外す", - "data.filter.searchBar.changeAllFiltersTitle": "すべてのフィルターの変更", - "data.indexPatterns.unableWriteLabel": "インデックスパターンを書き込めません!このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", - "data.indexPatterns.unknownFieldErrorMessage": "インデックスパターン「{title}」のフィールド「{name}」が不明なフィールドタイプを使用しています。", - "data.indexPatterns.unknownFieldHeader": "不明なフィールドタイプ {type}", - "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "無効なカレンダー間隔:{interval}、1よりも大きな値が必要です", - "data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "無効な間隔フォーマット:{interval}", - "data.query.queryBar.comboboxAriaLabel": "{pageType} ページの検索とフィルタリング", - "data.query.queryBar.kqlFullLanguageName": "Kibana クエリ言語", - "data.query.queryBar.kqlLanguageName": "KQL", - "data.query.queryBar.kqlOffLabel": "オフ", - "data.query.queryBar.kqlOnLabel": "オン", - "data.query.queryBar.luceneLanguageName": "Lucene", - "data.query.queryBar.luceneSyntaxWarningMessage": "Lucene クエリ構文を使用しているようですが、Kibana クエリ言語 (KQL) が選択されています。KQL ドキュメント {link} を確認してください。", - "data.query.queryBar.luceneSyntaxWarningOptOutText": "今後表示しない", - "data.query.queryBar.luceneSyntaxWarningTitle": "Lucene 構文警告", - "data.query.queryBar.searchInputAriaLabel": "{pageType} ページの検索とフィルタリングを行うには入力を開始してください", - "data.query.queryBar.searchInputPlaceholder": "検索", - "data.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) は、シンプルなクエリ構文とスクリプトフィールドのサポートを提供します。また、KQL はベーシックライセンス以上をご利用の場合、自動入力も提供します。KQL をオフにすると、Kibana は Lucene を使用します。", - "data.query.queryBar.syntaxOptionsDescription.docsLinkText": "こちら", - "data.query.queryBar.syntaxOptionsTitle": "構文オプション", - "data.search.searchBar.savedQueryDescriptionLabelText": "説明", - "data.search.searchBar.savedQueryDescriptionText": "再度使用するクエリテキストとフィルターを保存します。", - "data.search.searchBar.savedQueryForm.titleConflictText": "タイトルが既に保存されているクエリに使用されています", - "data.search.searchBar.savedQueryForm.titleMissingText": "名前が必要です", - "data.search.searchBar.savedQueryForm.whitespaceErrorText": "タイトルの始めと終わりにはスペースを使用できません", - "data.search.searchBar.savedQueryFormCancelButtonText": "キャンセル", - "data.search.searchBar.savedQueryFormSaveButtonText": "保存", - "data.search.searchBar.savedQueryFormTitle": "クエリを保存", - "data.search.searchBar.savedQueryIncludeFiltersLabelText": "フィルターを含める", - "data.search.searchBar.savedQueryIncludeTimeFilterLabelText": "時間フィルターを含める", - "data.search.searchBar.savedQueryNameHelpText": "名前が必要です。タイトルの始めと終わりにはスペースを使用できません。名前は固有でなければなりません。", - "data.search.searchBar.savedQueryNameLabelText": "名前", - "data.search.searchBar.savedQueryNoSavedQueriesText": "保存されたクエリがありません。", - "data.search.searchBar.savedQueryPopoverButtonText": "保存されたクエリを表示", - "data.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "現在保存されているクエリを消去", - "data.search.searchBar.savedQueryPopoverClearButtonText": "消去", - "data.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "キャンセル", - "data.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "削除", - "data.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "「{savedQueryName}」を削除しますか?", - "data.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "保存されたクエリ {savedQueryName} を削除", - "data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "新規保存クエリを保存", - "data.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "新規保存", - "data.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "新規保存クエリを保存", - "data.search.searchBar.savedQueryPopoverSaveButtonText": "現在のクエリを保存", - "data.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "{title} への変更を保存", - "data.search.searchBar.savedQueryPopoverSaveChangesButtonText": "変更を保存", - "data.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "保存クエリボタン {savedQueryName}", - "data.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName} の説明", - "data.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "選択されたクエリボタン {savedQueryName} を保存しました。変更を破棄するには押してください。", - "data.search.searchBar.savedQueryPopoverTitleText": "保存されたクエリ", "embeddableApi.actions.applyFilterActionTitle": "現在のビューにフィルターを適用", "embeddableApi.addPanel.createNewDefaultOption": "新規作成...", "embeddableApi.addPanel.displayName": "パネルの追加", @@ -1286,8 +1286,6 @@ "kbn.home.welcomeDescription": "Elastic Stack への開かれた窓", "kbn.home.welcomeHomePageHeader": "Kibana ホーム", "kbn.home.welcomeTitle": "Kibana へようこそ", - "advancedSettings.badge.readOnly.text": "読み込み専用", - "advancedSettings.badge.readOnly.tooltip": "高度な設定を保存できません", "kbn.management.createIndexPattern.betaLabel": "ベータ", "kbn.management.createIndexPattern.emptyState.checkDataButton": "新規データを確認", "kbn.management.createIndexPattern.emptyStateHeader": "Elasticsearch データが見つかりませんでした", @@ -1435,8 +1433,6 @@ "kbn.management.indexPattern.confirmOverwriteTitle": "{type} を上書きしますか?", "kbn.management.indexPattern.sectionsHeader": "インデックスパターン", "kbn.management.indexPattern.titleExistsLabel": "「{title}」というタイトルのインデックスパターンが既に存在します。", - "management.indexPatternHeader": "インデックスパターン", - "management.indexPatternLabel": "Elasticsearch からのデータの取得に役立つインデックスパターンを管理します。", "kbn.management.indexPatternList.createButton.betaLabel": "ベータ", "kbn.management.indexPatternPrompt.exampleOne": "チャートを作成したりコンテンツを素早くクエリできるように log-west-001 という名前の単一のデータソースをインデックスします。", "kbn.management.indexPatternPrompt.exampleOneTitle": "単一のデータソース", @@ -1555,9 +1551,7 @@ "kbn.management.objects.objectsTable.table.typeFilterName": "タイプ", "kbn.management.objects.objectsTable.unableFindSavedObjectsNotificationMessage": "保存されたオブジェクトが見つかりません", "kbn.management.objects.parsingFieldErrorMessage": "{fieldName} をインデックスパターン {indexName} 用にパース中にエラーが発生しました: {errorMessage}", - "management.objects.savedObjectsDescription": "保存された検索、ビジュアライゼーション、ダッシュボードのインポート、エクスポート、管理を行います", "kbn.management.objects.savedObjectsSectionLabel": "保存されたオブジェクト", - "management.objects.savedObjectsTitle": "保存されたオブジェクト", "kbn.management.objects.view.cancelButtonAriaLabel": "キャンセル", "kbn.management.objects.view.cancelButtonLabel": "キャンセル", "kbn.management.objects.view.deleteItemButtonLabel": "{title} を削除", @@ -1575,50 +1569,7 @@ "kbn.management.objects.view.viewItemTitle": "{title} を表示", "kbn.management.savedObjects.editBreadcrumb": "{savedObjectType} を編集", "kbn.management.savedObjects.indexBreadcrumb": "保存されたオブジェクト", - "advancedSettings.advancedSettingsLabel": "高度な設定", - "advancedSettings.callOutCautionDescription": "これらの設定は非常に上級ユーザー向けなのでご注意ください。ここでの変更は Kibana の重要な部分に不具合を生じさせる可能性があります。これらの設定はドキュメントに記載されていなかったり、サポートされていなかったり、実験的であったりします。フィールドにデフォルトの値が設定されている場合、そのフィールドを未入力のままにするとデフォルトに戻り、他の構成により利用できない可能性があります。カスタム設定を削除すると、Kibana の構成から永久に削除されます。", - "advancedSettings.callOutCautionTitle": "注意:不具合が起こる可能性があります", - "advancedSettings.categoryNames.dashboardLabel": "ダッシュボード", - "advancedSettings.categoryNames.discoverLabel": "ディスカバリ", - "advancedSettings.categoryNames.generalLabel": "一般", - "advancedSettings.categoryNames.notificationsLabel": "通知", - "advancedSettings.categoryNames.reportingLabel": "レポート", - "advancedSettings.categoryNames.searchLabel": "検索", - "advancedSettings.categoryNames.siemLabel": "SIEM", - "advancedSettings.categoryNames.timelionLabel": "Timelion", - "advancedSettings.categoryNames.visualizationsLabel": "ビジュアライゼーション", - "advancedSettings.categorySearchLabel": "カテゴリー", - "advancedSettings.field.cancelEditingButtonAriaLabel": "{ariaName} の編集をキャンセル", - "advancedSettings.field.cancelEditingButtonLabel": "キャンセル", - "advancedSettings.field.changeImageLinkAriaLabel": "{ariaName} を変更", - "advancedSettings.field.changeImageLinkText": "画像を変更", - "advancedSettings.field.codeEditorSyntaxErrorMessage": "無効な JSON 構文", - "advancedSettings.field.customSettingAriaLabel": "カスタム設定", - "advancedSettings.field.customSettingTooltip": "カスタム設定", - "advancedSettings.field.defaultValueText": "デフォルト: {value}", - "advancedSettings.field.defaultValueTypeJsonText": "デフォルト: {value}", - "advancedSettings.field.helpText": "この設定は Kibana サーバーにより上書きされ、変更することはできません。", - "advancedSettings.field.imageChangeErrorMessage": "画像を保存できませんでした", - "advancedSettings.field.imageTooLargeErrorMessage": "画像が大きすぎます。最大サイズは {maxSizeDescription} です", - "advancedSettings.field.offLabel": "オフ", - "advancedSettings.field.onLabel": "オン", - "advancedSettings.field.requiresPageReloadToastButtonLabel": "ページを再読み込み", - "advancedSettings.field.requiresPageReloadToastDescription": "「{settingName}」設定を有効にするには、ページを再読み込みしてください。", - "advancedSettings.field.resetFieldErrorMessage": "{name} をリセットできませんでした", - "advancedSettings.field.resetToDefaultLinkAriaLabel": "{ariaName} をデフォルトにリセット", - "advancedSettings.field.resetToDefaultLinkText": "デフォルトにリセット", - "advancedSettings.field.saveButtonAriaLabel": "{ariaName} を保存", - "advancedSettings.field.saveButtonLabel": "保存", - "advancedSettings.field.saveFieldErrorMessage": "{name} を保存できませんでした", - "advancedSettings.form.clearNoSearchResultText": "(検索結果を消去)", - "advancedSettings.form.clearSearchResultText": "(検索結果を消去)", - "advancedSettings.form.noSearchResultText": "設定が見つかりませんでした {clearSearch}", - "advancedSettings.form.searchResultText": "検索用語により {settingsCount} 件の設定が非表示になっています {clearSearch}", - "advancedSettings.pageTitle": "設定", - "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", - "advancedSettings.searchBarAriaLabel": "高度な設定を検索", "kbn.managementTitle": "管理", - "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "{query} を検索しました。{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other { # オプション}}があります。", "kbn.topNavMenu.openInspectorButtonLabel": "検査", "kbn.topNavMenu.refreshButtonLabel": "更新", "kbn.topNavMenu.saveVisualizationButtonLabel": "保存", @@ -1661,6 +1612,55 @@ "kbn.visualize.wizard.step1Breadcrumb": "作成", "kbn.visualize.wizard.step2Breadcrumb": "作成", "kbn.visualizeTitle": "可視化", + "advancedSettings.badge.readOnly.text": "読み込み専用", + "advancedSettings.badge.readOnly.tooltip": "高度な設定を保存できません", + "advancedSettings.advancedSettingsLabel": "高度な設定", + "advancedSettings.callOutCautionDescription": "これらの設定は非常に上級ユーザー向けなのでご注意ください。ここでの変更は Kibana の重要な部分に不具合を生じさせる可能性があります。これらの設定はドキュメントに記載されていなかったり、サポートされていなかったり、実験的であったりします。フィールドにデフォルトの値が設定されている場合、そのフィールドを未入力のままにするとデフォルトに戻り、他の構成により利用できない可能性があります。カスタム設定を削除すると、Kibana の構成から永久に削除されます。", + "advancedSettings.callOutCautionTitle": "注意:不具合が起こる可能性があります", + "advancedSettings.categoryNames.dashboardLabel": "ダッシュボード", + "advancedSettings.categoryNames.discoverLabel": "ディスカバリ", + "advancedSettings.categoryNames.generalLabel": "一般", + "advancedSettings.categoryNames.notificationsLabel": "通知", + "advancedSettings.categoryNames.reportingLabel": "レポート", + "advancedSettings.categoryNames.searchLabel": "検索", + "advancedSettings.categoryNames.siemLabel": "SIEM", + "advancedSettings.categoryNames.timelionLabel": "Timelion", + "advancedSettings.categoryNames.visualizationsLabel": "ビジュアライゼーション", + "advancedSettings.categorySearchLabel": "カテゴリー", + "advancedSettings.field.cancelEditingButtonAriaLabel": "{ariaName} の編集をキャンセル", + "advancedSettings.field.cancelEditingButtonLabel": "キャンセル", + "advancedSettings.field.changeImageLinkAriaLabel": "{ariaName} を変更", + "advancedSettings.field.changeImageLinkText": "画像を変更", + "advancedSettings.field.codeEditorSyntaxErrorMessage": "無効な JSON 構文", + "advancedSettings.field.customSettingAriaLabel": "カスタム設定", + "advancedSettings.field.customSettingTooltip": "カスタム設定", + "advancedSettings.field.defaultValueText": "デフォルト: {value}", + "advancedSettings.field.defaultValueTypeJsonText": "デフォルト: {value}", + "advancedSettings.field.helpText": "この設定は Kibana サーバーにより上書きされ、変更することはできません。", + "advancedSettings.field.imageChangeErrorMessage": "画像を保存できませんでした", + "advancedSettings.field.imageTooLargeErrorMessage": "画像が大きすぎます。最大サイズは {maxSizeDescription} です", + "advancedSettings.field.offLabel": "オフ", + "advancedSettings.field.onLabel": "オン", + "advancedSettings.field.requiresPageReloadToastButtonLabel": "ページを再読み込み", + "advancedSettings.field.requiresPageReloadToastDescription": "「{settingName}」設定を有効にするには、ページを再読み込みしてください。", + "advancedSettings.field.resetFieldErrorMessage": "{name} をリセットできませんでした", + "advancedSettings.field.resetToDefaultLinkAriaLabel": "{ariaName} をデフォルトにリセット", + "advancedSettings.field.resetToDefaultLinkText": "デフォルトにリセット", + "advancedSettings.field.saveButtonAriaLabel": "{ariaName} を保存", + "advancedSettings.field.saveButtonLabel": "保存", + "advancedSettings.field.saveFieldErrorMessage": "{name} を保存できませんでした", + "advancedSettings.form.clearNoSearchResultText": "(検索結果を消去)", + "advancedSettings.form.clearSearchResultText": "(検索結果を消去)", + "advancedSettings.form.noSearchResultText": "設定が見つかりませんでした {clearSearch}", + "advancedSettings.form.searchResultText": "検索用語により {settingsCount} 件の設定が非表示になっています {clearSearch}", + "advancedSettings.pageTitle": "設定", + "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", + "advancedSettings.searchBarAriaLabel": "高度な設定を検索", + "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "{query} を検索しました。{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other { # オプション}}があります。", + "management.indexPatternHeader": "インデックスパターン", + "management.indexPatternLabel": "Elasticsearch からのデータの取得に役立つインデックスパターンを管理します。", + "management.objects.savedObjectsDescription": "保存された検索、ビジュアライゼーション、ダッシュボードのインポート、エクスポート、管理を行います", + "management.objects.savedObjectsTitle": "保存されたオブジェクト", "kibana_legacy.bigUrlWarningNotificationMessage": "{advancedSettingsLink}で{storeInSessionStorageParam}オプションを有効にするか、オンスクリーンビジュアルを簡素化してください。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高度な設定", "kibana_legacy.bigUrlWarningNotificationTitle": "URLが大きく、Kibanaの動作が停止する可能性があります", @@ -3914,13 +3914,6 @@ "xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigFailedTitle": "構成を削除できませんでした", "xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededText": "「{serviceName}」の構成が正常に削除されました。エージェントに反映されるまでに少し時間がかかります。", "xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededTitle": "構成が削除されました", - "xpack.apm.settings.agentConf.flyOut.serviceSection.alreadyConfiguredOption": "既に構成済み", - "xpack.apm.settings.agentConf.flyOut.serviceSection.selectPlaceholder": "選択してください", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceEnvironmentSelectHelpText": "構成ごとに 1 つの環境のみがサポートされます。", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceEnvironmentSelectLabel": "環境", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceNameSelectHelpText": "構成するサービスを選択してください。", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceNameSelectLabel": "名前", - "xpack.apm.settings.agentConf.flyOut.serviceSection.title": "サービス", "xpack.apm.settings.agentConf.flyOut.settingsSection.captureBodyInputHelpText": "HTTP リクエストのトランザクションの場合、エージェントはリクエスト本文 (POST 変数など) をキャプチャすることができます。デフォルトは「off」です。", "xpack.apm.settings.agentConf.flyOut.settingsSection.captureBodyInputLabel": "本文をキャプチャ", "xpack.apm.settings.agentConf.flyOut.settingsSection.captureBodyInputPlaceholderText": "オプションを選択", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 25382221716ddb..ce1c713adc4bc7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -78,109 +78,6 @@ "messages": { "common.ui.aggResponse.allDocsTitle": "所有文档", "common.ui.aggTypes.rangesFormatMessage": "{gte} {from} 且 {lt} {to}", - "data.search.aggs.aggGroups.bucketsText": "存储桶", - "data.search.aggs.aggGroups.metricsText": "指标", - "data.search.aggs.buckets.dateHistogramLabel": "{fieldName}/{intervalDescription}", - "data.search.aggs.buckets.dateHistogramTitle": "Date Histogram", - "data.search.aggs.buckets.dateRangeTitle": "日期范围", - "data.search.aggs.buckets.filtersTitle": "筛选", - "data.search.aggs.buckets.filterTitle": "筛选", - "data.search.aggs.buckets.geohashGridTitle": "Geohash", - "data.search.aggs.buckets.geotileGridTitle": "地理磁贴", - "data.search.aggs.buckets.histogramTitle": "Histogram", - "data.search.aggs.buckets.intervalOptions.autoDisplayName": "自动", - "data.search.aggs.buckets.intervalOptions.dailyDisplayName": "每日", - "data.search.aggs.buckets.intervalOptions.hourlyDisplayName": "每小时", - "data.search.aggs.buckets.intervalOptions.millisecondDisplayName": "毫秒", - "data.search.aggs.buckets.intervalOptions.minuteDisplayName": "分钟", - "data.search.aggs.buckets.intervalOptions.monthlyDisplayName": "每月", - "data.search.aggs.buckets.intervalOptions.secondDisplayName": "秒", - "data.search.aggs.buckets.intervalOptions.weeklyDisplayName": "每周", - "data.search.aggs.buckets.intervalOptions.yearlyDisplayName": "每年", - "data.search.aggs.buckets.ipRangeLabel": "{fieldName} IP 范围", - "data.search.aggs.buckets.ipRangeTitle": "IPv4 范围", - "data.search.aggs.aggTypes.rangesFormatMessage": "{gte} {from} 且 {lt} {to}", - "data.search.aggs.aggTypesLabel": "{fieldName} 范围", - "data.search.aggs.buckets.rangeTitle": "范围", - "data.search.aggs.buckets.significantTerms.excludeLabel": "排除", - "data.search.aggs.buckets.significantTerms.includeLabel": "包括", - "data.search.aggs.buckets.significantTermsLabel": "{fieldName} 中排名前 {size} 的罕见词", - "data.search.aggs.buckets.significantTermsTitle": "重要词", - "data.search.aggs.buckets.terms.excludeLabel": "排除", - "data.search.aggs.buckets.terms.includeLabel": "包括", - "data.search.aggs.buckets.terms.missingBucketLabel": "缺失", - "data.search.aggs.buckets.terms.orderAscendingTitle": "升序", - "data.search.aggs.buckets.terms.orderDescendingTitle": "降序", - "data.search.aggs.buckets.terms.otherBucketDescription": "此请求计数不符合数据存储桶条件的文档数目。", - "data.search.aggs.buckets.terms.otherBucketLabel": "其他", - "data.search.aggs.buckets.terms.otherBucketTitle": "其他存储桶", - "data.search.aggs.buckets.termsTitle": "词", - "data.search.aggs.histogram.missingMaxMinValuesWarning": "无法检索最大值和最小值以自动缩放直方图存储桶。这可能会导致可视化性能低下。", - "data.search.aggs.metrics.averageBucketTitle": "平均存储桶", - "data.search.aggs.metrics.averageLabel": "{field}平均值", - "data.search.aggs.metrics.averageTitle": "平均值", - "data.search.aggs.metrics.bucketAggTitle": "存储桶聚合", - "data.search.aggs.metrics.countLabel": "计数", - "data.search.aggs.metrics.countTitle": "计数", - "data.search.aggs.metrics.cumulativeSumLabel": "累计和", - "data.search.aggs.metrics.cumulativeSumTitle": "累计和", - "data.search.aggs.metrics.derivativeLabel": "导数", - "data.search.aggs.metrics.derivativeTitle": "导数", - "data.search.aggs.metrics.geoBoundsLabel": "地理边界", - "data.search.aggs.metrics.geoBoundsTitle": "地理边界", - "data.search.aggs.metrics.geoCentroidLabel": "地理重心", - "data.search.aggs.metrics.geoCentroidTitle": "地理重心", - "data.search.aggs.metrics.maxBucketTitle": "最大存储桶", - "data.search.aggs.metrics.maxLabel": "{field}最大值", - "data.search.aggs.metrics.maxTitle": "最大值", - "data.search.aggs.metrics.medianLabel": "{field}中值", - "data.search.aggs.metrics.medianTitle": "中值", - "data.search.aggs.metrics.metricAggregationsSubtypeTitle": "指标聚合", - "data.search.aggs.metrics.metricAggTitle": "指标聚合", - "data.search.aggs.metrics.minBucketTitle": "最小存储桶", - "data.search.aggs.metrics.minLabel": "{field}最小值", - "data.search.aggs.metrics.minTitle": "最小值", - "data.search.aggs.metrics.movingAvgLabel": "移动平均值", - "data.search.aggs.metrics.movingAvgTitle": "移动平均值", - "data.search.aggs.metrics.overallAverageLabel": "总体平均值", - "data.search.aggs.metrics.overallMaxLabel": "总体最大值", - "data.search.aggs.metrics.overallMinLabel": "总体最大值", - "data.search.aggs.metrics.overallSumLabel": "总和", - "data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle": "父级管道聚合", - "data.search.aggs.metrics.percentileRanks.valuePropsLabel": "“{label}” 的百分位数排名 {format}", - "data.search.aggs.metrics.percentileRanksLabel": "“{field}” 的百分位数排名", - "data.search.aggs.metrics.percentileRanksTitle": "百分位数排名", - "data.search.aggs.metrics.percentiles.valuePropsLabel": "“{label}” 的 {percentile} 百分位数", - "data.search.aggs.metrics.percentilesLabel": "“{field}” 的百分位数", - "data.search.aggs.metrics.percentilesTitle": "百分位数", - "data.search.aggs.metrics.serialDiffLabel": "序列差异", - "data.search.aggs.metrics.serialDiffTitle": "序列差异", - "data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle": "同级管道聚合", - "data.search.aggs.metrics.standardDeviation.keyDetailsLabel": "“{fieldDisplayName}” 的标准偏差", - "data.search.aggs.metrics.standardDeviation.lowerKeyDetailsTitle": "下{label}", - "data.search.aggs.metrics.standardDeviation.upperKeyDetailsTitle": "上{label}", - "data.search.aggs.metrics.standardDeviationLabel": "“{field}” 的标准偏差", - "data.search.aggs.metrics.standardDeviationTitle": "标准偏差", - "data.search.aggs.metrics.sumBucketTitle": "求和存储桶", - "data.search.aggs.metrics.sumLabel": "“{field}” 的和", - "data.search.aggs.metrics.sumTitle": "和", - "data.search.aggs.metrics.topHit.ascendingLabel": "升序", - "data.search.aggs.metrics.topHit.averageLabel": "平均值", - "data.search.aggs.metrics.topHit.concatenateLabel": "连接", - "data.search.aggs.metrics.topHit.descendingLabel": "降序", - "data.search.aggs.metrics.topHit.firstPrefixLabel": "第一", - "data.search.aggs.metrics.topHit.lastPrefixLabel": "最后", - "data.search.aggs.metrics.topHit.maxLabel": "最大值", - "data.search.aggs.metrics.topHit.minLabel": "最小值", - "data.search.aggs.metrics.topHit.sumLabel": "和", - "data.search.aggs.metrics.topHitTitle": "最高命中结果", - "data.search.aggs.metrics.uniqueCountLabel": "“{field}” 的唯一计数", - "data.search.aggs.metrics.uniqueCountTitle": "唯一计数", - "data.search.aggs.otherBucket.labelForMissingValuesLabel": "缺失值的标签", - "data.search.aggs.otherBucket.labelForOtherBucketLabel": "其他存储桶的标签", - "data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "已保存的 {fieldParameter} 参数现在无效。请选择新字段。", - "data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} 是必需字段", - "data.search.aggs.string.customLabel": "定制标签", "common.ui.directives.paginate.size.allDropDownOptionLabel": "全部", "common.ui.dualRangeControl.mustSetBothErrorMessage": "下限值和上限值都须设置", "common.ui.dualRangeControl.outsideOfRangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", @@ -367,10 +264,223 @@ "common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,并且似乎没有任何可安全删除的项。\n\n通常可通过移至新的标签页来解决此问题,但这会导致更大的问题。如果您有规律地看到此消息,请在 {gitHubIssuesUrl} 提交问题。", "common.ui.url.replacementFailedErrorMessage": "替换失败,未解析的表达式:{expr}", "common.ui.url.savedObjectIsMissingNotificationMessage": "已保存对象缺失", - "data.search.aggs.percentageOfLabel": "{label} 的百分比", "common.ui.vis.defaultFeedbackMessage": "想反馈?请在“{link}中创建问题。", "common.ui.vis.kibanaMap.leaflet.fitDataBoundsAriaLabel": "适应数据边界", "common.ui.vis.kibanaMap.zoomWarning": "已达到缩放级别最大数目。要一直放大,请升级到 Elasticsearch 和 Kibana 的 {defaultDistribution}。您可以通过 {ems} 免费使用其他缩放级别。或者,您可以配置自己的地图服务器。请前往 { wms } 或 { configSettings} 以获取详细信息。", + "data.search.aggs.aggGroups.bucketsText": "存储桶", + "data.search.aggs.aggGroups.metricsText": "指标", + "data.search.aggs.buckets.dateHistogramLabel": "{fieldName}/{intervalDescription}", + "data.search.aggs.buckets.dateHistogramTitle": "Date Histogram", + "data.search.aggs.buckets.dateRangeTitle": "日期范围", + "data.search.aggs.buckets.filtersTitle": "筛选", + "data.search.aggs.buckets.filterTitle": "筛选", + "data.search.aggs.buckets.geohashGridTitle": "Geohash", + "data.search.aggs.buckets.geotileGridTitle": "地理磁贴", + "data.search.aggs.buckets.histogramTitle": "Histogram", + "data.search.aggs.buckets.intervalOptions.autoDisplayName": "自动", + "data.search.aggs.buckets.intervalOptions.dailyDisplayName": "每日", + "data.search.aggs.buckets.intervalOptions.hourlyDisplayName": "每小时", + "data.search.aggs.buckets.intervalOptions.millisecondDisplayName": "毫秒", + "data.search.aggs.buckets.intervalOptions.minuteDisplayName": "分钟", + "data.search.aggs.buckets.intervalOptions.monthlyDisplayName": "每月", + "data.search.aggs.buckets.intervalOptions.secondDisplayName": "秒", + "data.search.aggs.buckets.intervalOptions.weeklyDisplayName": "每周", + "data.search.aggs.buckets.intervalOptions.yearlyDisplayName": "每年", + "data.search.aggs.buckets.ipRangeLabel": "{fieldName} IP 范围", + "data.search.aggs.buckets.ipRangeTitle": "IPv4 范围", + "data.search.aggs.aggTypes.rangesFormatMessage": "{gte} {from} 且 {lt} {to}", + "data.search.aggs.aggTypesLabel": "{fieldName} 范围", + "data.search.aggs.buckets.rangeTitle": "范围", + "data.search.aggs.buckets.significantTerms.excludeLabel": "排除", + "data.search.aggs.buckets.significantTerms.includeLabel": "包括", + "data.search.aggs.buckets.significantTermsLabel": "{fieldName} 中排名前 {size} 的罕见词", + "data.search.aggs.buckets.significantTermsTitle": "重要词", + "data.search.aggs.buckets.terms.excludeLabel": "排除", + "data.search.aggs.buckets.terms.includeLabel": "包括", + "data.search.aggs.buckets.terms.missingBucketLabel": "缺失", + "data.search.aggs.buckets.terms.orderAscendingTitle": "升序", + "data.search.aggs.buckets.terms.orderDescendingTitle": "降序", + "data.search.aggs.buckets.terms.otherBucketDescription": "此请求计数不符合数据存储桶条件的文档数目。", + "data.search.aggs.buckets.terms.otherBucketLabel": "其他", + "data.search.aggs.buckets.terms.otherBucketTitle": "其他存储桶", + "data.search.aggs.buckets.termsTitle": "词", + "data.search.aggs.histogram.missingMaxMinValuesWarning": "无法检索最大值和最小值以自动缩放直方图存储桶。这可能会导致可视化性能低下。", + "data.search.aggs.metrics.averageBucketTitle": "平均存储桶", + "data.search.aggs.metrics.averageLabel": "{field}平均值", + "data.search.aggs.metrics.averageTitle": "平均值", + "data.search.aggs.metrics.bucketAggTitle": "存储桶聚合", + "data.search.aggs.metrics.countLabel": "计数", + "data.search.aggs.metrics.countTitle": "计数", + "data.search.aggs.metrics.cumulativeSumLabel": "累计和", + "data.search.aggs.metrics.cumulativeSumTitle": "累计和", + "data.search.aggs.metrics.derivativeLabel": "导数", + "data.search.aggs.metrics.derivativeTitle": "导数", + "data.search.aggs.metrics.geoBoundsLabel": "地理边界", + "data.search.aggs.metrics.geoBoundsTitle": "地理边界", + "data.search.aggs.metrics.geoCentroidLabel": "地理重心", + "data.search.aggs.metrics.geoCentroidTitle": "地理重心", + "data.search.aggs.metrics.maxBucketTitle": "最大存储桶", + "data.search.aggs.metrics.maxLabel": "{field}最大值", + "data.search.aggs.metrics.maxTitle": "最大值", + "data.search.aggs.metrics.medianLabel": "{field}中值", + "data.search.aggs.metrics.medianTitle": "中值", + "data.search.aggs.metrics.metricAggregationsSubtypeTitle": "指标聚合", + "data.search.aggs.metrics.metricAggTitle": "指标聚合", + "data.search.aggs.metrics.minBucketTitle": "最小存储桶", + "data.search.aggs.metrics.minLabel": "{field}最小值", + "data.search.aggs.metrics.minTitle": "最小值", + "data.search.aggs.metrics.movingAvgLabel": "移动平均值", + "data.search.aggs.metrics.movingAvgTitle": "移动平均值", + "data.search.aggs.metrics.overallAverageLabel": "总体平均值", + "data.search.aggs.metrics.overallMaxLabel": "总体最大值", + "data.search.aggs.metrics.overallMinLabel": "总体最大值", + "data.search.aggs.metrics.overallSumLabel": "总和", + "data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle": "父级管道聚合", + "data.search.aggs.metrics.percentileRanks.valuePropsLabel": "“{label}” 的百分位数排名 {format}", + "data.search.aggs.metrics.percentileRanksLabel": "“{field}” 的百分位数排名", + "data.search.aggs.metrics.percentileRanksTitle": "百分位数排名", + "data.search.aggs.metrics.percentiles.valuePropsLabel": "“{label}” 的 {percentile} 百分位数", + "data.search.aggs.metrics.percentilesLabel": "“{field}” 的百分位数", + "data.search.aggs.metrics.percentilesTitle": "百分位数", + "data.search.aggs.metrics.serialDiffLabel": "序列差异", + "data.search.aggs.metrics.serialDiffTitle": "序列差异", + "data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle": "同级管道聚合", + "data.search.aggs.metrics.standardDeviation.keyDetailsLabel": "“{fieldDisplayName}” 的标准偏差", + "data.search.aggs.metrics.standardDeviation.lowerKeyDetailsTitle": "下{label}", + "data.search.aggs.metrics.standardDeviation.upperKeyDetailsTitle": "上{label}", + "data.search.aggs.metrics.standardDeviationLabel": "“{field}” 的标准偏差", + "data.search.aggs.metrics.standardDeviationTitle": "标准偏差", + "data.search.aggs.metrics.sumBucketTitle": "求和存储桶", + "data.search.aggs.metrics.sumLabel": "“{field}” 的和", + "data.search.aggs.metrics.sumTitle": "和", + "data.search.aggs.metrics.topHit.ascendingLabel": "升序", + "data.search.aggs.metrics.topHit.averageLabel": "平均值", + "data.search.aggs.metrics.topHit.concatenateLabel": "连接", + "data.search.aggs.metrics.topHit.descendingLabel": "降序", + "data.search.aggs.metrics.topHit.firstPrefixLabel": "第一", + "data.search.aggs.metrics.topHit.lastPrefixLabel": "最后", + "data.search.aggs.metrics.topHit.maxLabel": "最大值", + "data.search.aggs.metrics.topHit.minLabel": "最小值", + "data.search.aggs.metrics.topHit.sumLabel": "和", + "data.search.aggs.metrics.topHitTitle": "最高命中结果", + "data.search.aggs.metrics.uniqueCountLabel": "“{field}” 的唯一计数", + "data.search.aggs.metrics.uniqueCountTitle": "唯一计数", + "data.search.aggs.otherBucket.labelForMissingValuesLabel": "缺失值的标签", + "data.search.aggs.otherBucket.labelForOtherBucketLabel": "其他存储桶的标签", + "data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "已保存的 {fieldParameter} 参数现在无效。请选择新字段。", + "data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} 是必需字段", + "data.search.aggs.string.customLabel": "定制标签", + "data.search.aggs.percentageOfLabel": "{label} 的百分比", + "data.filter.applyFilters.popupHeader": "选择要应用的筛选", + "data.filter.applyFiltersPopup.cancelButtonLabel": "取消", + "data.filter.applyFiltersPopup.saveButtonLabel": "应用", + "data.filter.filterBar.addFilterButtonLabel": "添加筛选", + "data.filter.filterBar.deleteFilterButtonLabel": "删除", + "data.filter.filterBar.disabledFilterPrefix": "已禁用", + "data.filter.filterBar.disableFilterButtonLabel": "暂时禁用", + "data.filter.filterBar.editFilterButtonLabel": "编辑筛选", + "data.filter.filterBar.enableFilterButtonLabel": "重新启用", + "data.filter.filterBar.excludeFilterButtonLabel": "排除结果", + "data.filter.filterBar.filterItemBadgeAriaLabel": "筛选操作", + "data.filter.filterBar.filterItemBadgeIconAriaLabel": "删除", + "data.filter.filterBar.includeFilterButtonLabel": "包括结果", + "data.filter.filterBar.indexPatternSelectPlaceholder": "选择索引模式", + "data.filter.filterBar.moreFilterActionsMessage": "筛选:{innerText}。选择以获取更多筛选操作。", + "data.filter.filterBar.negatedFilterPrefix": "非 ", + "data.filter.filterBar.pinFilterButtonLabel": "在所有应用上固定", + "data.filter.filterBar.pinnedFilterPrefix": "已固定", + "data.filter.filterBar.unpinFilterButtonLabel": "取消固定", + "data.filter.filterEditor.cancelButtonLabel": "取消", + "data.filter.filterEditor.createCustomLabelInputLabel": "定制标签", + "data.filter.filterEditor.createCustomLabelSwitchLabel": "创建定制标签?", + "data.filter.filterEditor.dateFormatHelpLinkLabel": "已接受日期格式", + "data.filter.filterEditor.doesNotExistOperatorOptionLabel": "不存在", + "data.filter.filterEditor.editFilterPopupTitle": "编辑筛选", + "data.filter.filterEditor.editFilterValuesButtonLabel": "编辑筛选值", + "data.filter.filterEditor.editQueryDslButtonLabel": "编辑为查询 DSL", + "data.filter.filterEditor.existsOperatorOptionLabel": "存在", + "data.filter.filterEditor.falseOptionLabel": "false", + "data.filter.filterEditor.fieldSelectLabel": "字段", + "data.filter.filterEditor.fieldSelectPlaceholder": "首先选择字段", + "data.filter.filterEditor.indexPatternSelectLabel": "索引模式", + "data.filter.filterEditor.isBetweenOperatorOptionLabel": "介于", + "data.filter.filterEditor.isNotBetweenOperatorOptionLabel": "不介于", + "data.filter.filterEditor.isNotOneOfOperatorOptionLabel": "不属于", + "data.filter.filterEditor.isNotOperatorOptionLabel": "不是", + "data.filter.filterEditor.isOneOfOperatorOptionLabel": "属于", + "data.filter.filterEditor.isOperatorOptionLabel": "是", + "data.filter.filterEditor.operatorSelectLabel": "运算符", + "data.filter.filterEditor.operatorSelectPlaceholderSelect": "选择", + "data.filter.filterEditor.operatorSelectPlaceholderWaiting": "正在等候", + "data.filter.filterEditor.queryDslLabel": "Elasticsearch 查询 DSL", + "data.filter.filterEditor.rangeEndInputPlaceholder": "范围结束", + "data.filter.filterEditor.rangeInputLabel": "范围", + "data.filter.filterEditor.rangeStartInputPlaceholder": "范围开始", + "data.filter.filterEditor.saveButtonLabel": "保存", + "data.filter.filterEditor.trueOptionLabel": "true", + "data.filter.filterEditor.valueInputLabel": "值", + "data.filter.filterEditor.valueInputPlaceholder": "输入值", + "data.filter.filterEditor.valueSelectPlaceholder": "选择值", + "data.filter.filterEditor.valuesSelectLabel": "值", + "data.filter.filterEditor.valuesSelectPlaceholder": "选择值", + "data.filter.options.changeAllFiltersButtonLabel": "更改所有筛选", + "data.filter.options.deleteAllFiltersButtonLabel": "全部删除", + "data.filter.options.disableAllFiltersButtonLabel": "全部禁用", + "data.filter.options.enableAllFiltersButtonLabel": "全部启用", + "data.filter.options.invertDisabledFiltersButtonLabel": "反向已启用/已禁用", + "data.filter.options.invertNegatedFiltersButtonLabel": "反向包括", + "data.filter.options.pinAllFiltersButtonLabel": "全部固定", + "data.filter.options.unpinAllFiltersButtonLabel": "全部取消固定", + "data.filter.searchBar.changeAllFiltersTitle": "更改所有筛选", + "data.indexPatterns.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", + "data.indexPatterns.unknownFieldErrorMessage": "indexPattern “{title}” 中的字段 “{name}” 使用未知字段类型。", + "data.indexPatterns.unknownFieldHeader": "未知字段类型 {type}", + "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "无效的日历时间间隔:{interval},值必须为 1", + "data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "时间间隔格式无效:{interval}", + "data.query.queryBar.comboboxAriaLabel": "搜索并筛选 {pageType} 页面", + "data.query.queryBar.kqlFullLanguageName": "Kibana 查询语言", + "data.query.queryBar.kqlLanguageName": "KQL", + "data.query.queryBar.kqlOffLabel": "关闭", + "data.query.queryBar.kqlOnLabel": "开启", + "data.query.queryBar.luceneLanguageName": "Lucene", + "data.query.queryBar.luceneSyntaxWarningMessage": "尽管您选择了 Kibana 查询语言 (KQL),但似乎您正在尝试使用 Lucene 查询语法。请查看 KQL 文档 {link}。", + "data.query.queryBar.luceneSyntaxWarningOptOutText": "不再显示", + "data.query.queryBar.luceneSyntaxWarningTitle": "Lucene 语法警告", + "data.query.queryBar.searchInputAriaLabel": "开始键入内容,以搜索并筛选 {pageType} 页面", + "data.query.queryBar.searchInputPlaceholder": "搜索", + "data.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) 提供简化查询语法并支持脚本字段。如果您具有基本许可或更高级别的许可,KQL 还提供自动填充功能。如果关闭 KQL,Kibana 将使用 Lucene。", + "data.query.queryBar.syntaxOptionsDescription.docsLinkText": "此处", + "data.query.queryBar.syntaxOptionsTitle": "语法选项", + "data.search.searchBar.savedQueryDescriptionLabelText": "描述", + "data.search.searchBar.savedQueryDescriptionText": "保存想要再次使用的查询文本和筛选。", + "data.search.searchBar.savedQueryForm.titleConflictText": "标题与现有已保存查询有冲突", + "data.search.searchBar.savedQueryForm.titleMissingText": "“名称”必填", + "data.search.searchBar.savedQueryForm.whitespaceErrorText": "标题不能包含前导或尾随空格", + "data.search.searchBar.savedQueryFormCancelButtonText": "取消", + "data.search.searchBar.savedQueryFormSaveButtonText": "保存", + "data.search.searchBar.savedQueryFormTitle": "保存查询", + "data.search.searchBar.savedQueryIncludeFiltersLabelText": "包括筛选", + "data.search.searchBar.savedQueryIncludeTimeFilterLabelText": "包括时间筛选", + "data.search.searchBar.savedQueryNameHelpText": "“名称”必填。标题不能包含前导或尾随空格。名称必须唯一。", + "data.search.searchBar.savedQueryNameLabelText": "名称", + "data.search.searchBar.savedQueryNoSavedQueriesText": "没有已保存查询。", + "data.search.searchBar.savedQueryPopoverButtonText": "查看已保存查询", + "data.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "清除当前已保存查询", + "data.search.searchBar.savedQueryPopoverClearButtonText": "清除", + "data.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "取消", + "data.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "删除", + "data.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "删除“{savedQueryName}”?", + "data.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "删除已保存查询 {savedQueryName}", + "data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "另存为新的已保存查询", + "data.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "另存为新的", + "data.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "保存新的已保存查询", + "data.search.searchBar.savedQueryPopoverSaveButtonText": "保存当前查询", + "data.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "将更改保存到 {title}", + "data.search.searchBar.savedQueryPopoverSaveChangesButtonText": "保存更改", + "data.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "已保存查询按钮 {savedQueryName}", + "data.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName} 描述", + "data.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "已保存查询按钮已选择 {savedQueryName}。按下可清除任何更改。", + "data.search.searchBar.savedQueryPopoverTitleText": "已保存查询", "charts.colormaps.bluesText": "蓝色", "charts.colormaps.greensText": "绿色", "charts.colormaps.greenToRedText": "绿到红", @@ -526,116 +636,6 @@ "dashboardEmbeddableContainer.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "全屏", "dashboardEmbeddableContainer.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "无法加载仪表板。", "dashboardEmbeddableContainer.factory.displayName": "仪表板", - "data.filter.applyFilters.popupHeader": "选择要应用的筛选", - "data.filter.applyFiltersPopup.cancelButtonLabel": "取消", - "data.filter.applyFiltersPopup.saveButtonLabel": "应用", - "data.filter.filterBar.addFilterButtonLabel": "添加筛选", - "data.filter.filterBar.deleteFilterButtonLabel": "删除", - "data.filter.filterBar.disabledFilterPrefix": "已禁用", - "data.filter.filterBar.disableFilterButtonLabel": "暂时禁用", - "data.filter.filterBar.editFilterButtonLabel": "编辑筛选", - "data.filter.filterBar.enableFilterButtonLabel": "重新启用", - "data.filter.filterBar.excludeFilterButtonLabel": "排除结果", - "data.filter.filterBar.filterItemBadgeAriaLabel": "筛选操作", - "data.filter.filterBar.filterItemBadgeIconAriaLabel": "删除", - "data.filter.filterBar.includeFilterButtonLabel": "包括结果", - "data.filter.filterBar.indexPatternSelectPlaceholder": "选择索引模式", - "data.filter.filterBar.moreFilterActionsMessage": "筛选:{innerText}。选择以获取更多筛选操作。", - "data.filter.filterBar.negatedFilterPrefix": "非 ", - "data.filter.filterBar.pinFilterButtonLabel": "在所有应用上固定", - "data.filter.filterBar.pinnedFilterPrefix": "已固定", - "data.filter.filterBar.unpinFilterButtonLabel": "取消固定", - "data.filter.filterEditor.cancelButtonLabel": "取消", - "data.filter.filterEditor.createCustomLabelInputLabel": "定制标签", - "data.filter.filterEditor.createCustomLabelSwitchLabel": "创建定制标签?", - "data.filter.filterEditor.dateFormatHelpLinkLabel": "已接受日期格式", - "data.filter.filterEditor.doesNotExistOperatorOptionLabel": "不存在", - "data.filter.filterEditor.editFilterPopupTitle": "编辑筛选", - "data.filter.filterEditor.editFilterValuesButtonLabel": "编辑筛选值", - "data.filter.filterEditor.editQueryDslButtonLabel": "编辑为查询 DSL", - "data.filter.filterEditor.existsOperatorOptionLabel": "存在", - "data.filter.filterEditor.falseOptionLabel": "false", - "data.filter.filterEditor.fieldSelectLabel": "字段", - "data.filter.filterEditor.fieldSelectPlaceholder": "首先选择字段", - "data.filter.filterEditor.indexPatternSelectLabel": "索引模式", - "data.filter.filterEditor.isBetweenOperatorOptionLabel": "介于", - "data.filter.filterEditor.isNotBetweenOperatorOptionLabel": "不介于", - "data.filter.filterEditor.isNotOneOfOperatorOptionLabel": "不属于", - "data.filter.filterEditor.isNotOperatorOptionLabel": "不是", - "data.filter.filterEditor.isOneOfOperatorOptionLabel": "属于", - "data.filter.filterEditor.isOperatorOptionLabel": "是", - "data.filter.filterEditor.operatorSelectLabel": "运算符", - "data.filter.filterEditor.operatorSelectPlaceholderSelect": "选择", - "data.filter.filterEditor.operatorSelectPlaceholderWaiting": "正在等候", - "data.filter.filterEditor.queryDslLabel": "Elasticsearch 查询 DSL", - "data.filter.filterEditor.rangeEndInputPlaceholder": "范围结束", - "data.filter.filterEditor.rangeInputLabel": "范围", - "data.filter.filterEditor.rangeStartInputPlaceholder": "范围开始", - "data.filter.filterEditor.saveButtonLabel": "保存", - "data.filter.filterEditor.trueOptionLabel": "true", - "data.filter.filterEditor.valueInputLabel": "值", - "data.filter.filterEditor.valueInputPlaceholder": "输入值", - "data.filter.filterEditor.valueSelectPlaceholder": "选择值", - "data.filter.filterEditor.valuesSelectLabel": "值", - "data.filter.filterEditor.valuesSelectPlaceholder": "选择值", - "data.filter.options.changeAllFiltersButtonLabel": "更改所有筛选", - "data.filter.options.deleteAllFiltersButtonLabel": "全部删除", - "data.filter.options.disableAllFiltersButtonLabel": "全部禁用", - "data.filter.options.enableAllFiltersButtonLabel": "全部启用", - "data.filter.options.invertDisabledFiltersButtonLabel": "反向已启用/已禁用", - "data.filter.options.invertNegatedFiltersButtonLabel": "反向包括", - "data.filter.options.pinAllFiltersButtonLabel": "全部固定", - "data.filter.options.unpinAllFiltersButtonLabel": "全部取消固定", - "data.filter.searchBar.changeAllFiltersTitle": "更改所有筛选", - "data.indexPatterns.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", - "data.indexPatterns.unknownFieldErrorMessage": "indexPattern “{title}” 中的字段 “{name}” 使用未知字段类型。", - "data.indexPatterns.unknownFieldHeader": "未知字段类型 {type}", - "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "无效的日历时间间隔:{interval},值必须为 1", - "data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "时间间隔格式无效:{interval}", - "data.query.queryBar.comboboxAriaLabel": "搜索并筛选 {pageType} 页面", - "data.query.queryBar.kqlFullLanguageName": "Kibana 查询语言", - "data.query.queryBar.kqlLanguageName": "KQL", - "data.query.queryBar.kqlOffLabel": "关闭", - "data.query.queryBar.kqlOnLabel": "开启", - "data.query.queryBar.luceneLanguageName": "Lucene", - "data.query.queryBar.luceneSyntaxWarningMessage": "尽管您选择了 Kibana 查询语言 (KQL),但似乎您正在尝试使用 Lucene 查询语法。请查看 KQL 文档 {link}。", - "data.query.queryBar.luceneSyntaxWarningOptOutText": "不再显示", - "data.query.queryBar.luceneSyntaxWarningTitle": "Lucene 语法警告", - "data.query.queryBar.searchInputAriaLabel": "开始键入内容,以搜索并筛选 {pageType} 页面", - "data.query.queryBar.searchInputPlaceholder": "搜索", - "data.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) 提供简化查询语法并支持脚本字段。如果您具有基本许可或更高级别的许可,KQL 还提供自动填充功能。如果关闭 KQL,Kibana 将使用 Lucene。", - "data.query.queryBar.syntaxOptionsDescription.docsLinkText": "此处", - "data.query.queryBar.syntaxOptionsTitle": "语法选项", - "data.search.searchBar.savedQueryDescriptionLabelText": "描述", - "data.search.searchBar.savedQueryDescriptionText": "保存想要再次使用的查询文本和筛选。", - "data.search.searchBar.savedQueryForm.titleConflictText": "标题与现有已保存查询有冲突", - "data.search.searchBar.savedQueryForm.titleMissingText": "“名称”必填", - "data.search.searchBar.savedQueryForm.whitespaceErrorText": "标题不能包含前导或尾随空格", - "data.search.searchBar.savedQueryFormCancelButtonText": "取消", - "data.search.searchBar.savedQueryFormSaveButtonText": "保存", - "data.search.searchBar.savedQueryFormTitle": "保存查询", - "data.search.searchBar.savedQueryIncludeFiltersLabelText": "包括筛选", - "data.search.searchBar.savedQueryIncludeTimeFilterLabelText": "包括时间筛选", - "data.search.searchBar.savedQueryNameHelpText": "“名称”必填。标题不能包含前导或尾随空格。名称必须唯一。", - "data.search.searchBar.savedQueryNameLabelText": "名称", - "data.search.searchBar.savedQueryNoSavedQueriesText": "没有已保存查询。", - "data.search.searchBar.savedQueryPopoverButtonText": "查看已保存查询", - "data.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "清除当前已保存查询", - "data.search.searchBar.savedQueryPopoverClearButtonText": "清除", - "data.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "取消", - "data.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "删除", - "data.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "删除“{savedQueryName}”?", - "data.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "删除已保存查询 {savedQueryName}", - "data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "另存为新的已保存查询", - "data.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "另存为新的", - "data.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "保存新的已保存查询", - "data.search.searchBar.savedQueryPopoverSaveButtonText": "保存当前查询", - "data.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "将更改保存到 {title}", - "data.search.searchBar.savedQueryPopoverSaveChangesButtonText": "保存更改", - "data.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "已保存查询按钮 {savedQueryName}", - "data.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName} 描述", - "data.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "已保存查询按钮已选择 {savedQueryName}。按下可清除任何更改。", - "data.search.searchBar.savedQueryPopoverTitleText": "已保存查询", "embeddableApi.actions.applyFilterActionTitle": "将筛选应用于当前视图", "embeddableApi.addPanel.createNewDefaultOption": "创建新的......", "embeddableApi.addPanel.displayName": "添加面板", @@ -1286,8 +1286,6 @@ "kbn.home.welcomeDescription": "您了解 Elastic Stack 的窗口", "kbn.home.welcomeHomePageHeader": "Kibana 主页", "kbn.home.welcomeTitle": "欢迎使用 Kibana", - "advancedSettings.badge.readOnly.text": "只读", - "advancedSettings.badge.readOnly.tooltip": "无法保存高级设置", "kbn.management.createIndexPattern.betaLabel": "公测版", "kbn.management.createIndexPattern.emptyState.checkDataButton": "检查新数据", "kbn.management.createIndexPattern.emptyStateHeader": "找不到任何 Elasticsearch 数据", @@ -1435,8 +1433,6 @@ "kbn.management.indexPattern.confirmOverwriteTitle": "覆盖“{type}”?", "kbn.management.indexPattern.sectionsHeader": "索引模式", "kbn.management.indexPattern.titleExistsLabel": "具有标题 “{title}” 的索引模式已存在。", - "management.indexPatternHeader": "索引模式", - "management.indexPatternLabel": "管理帮助从 Elasticsearch 检索数据的索引模式。", "kbn.management.indexPatternList.createButton.betaLabel": "公测版", "kbn.management.indexPatternPrompt.exampleOne": "索引单个称作 log-west-001 的数据源,以便可以快速地构建图表或查询其内容。", "kbn.management.indexPatternPrompt.exampleOneTitle": "单数据源", @@ -1555,9 +1551,7 @@ "kbn.management.objects.objectsTable.table.typeFilterName": "类型", "kbn.management.objects.objectsTable.unableFindSavedObjectsNotificationMessage": "找不到已保存对象", "kbn.management.objects.parsingFieldErrorMessage": "为索引模式 “{indexName}” 解析 “{fieldName}” 时发生错误:{errorMessage}", - "management.objects.savedObjectsDescription": "导入、导出和管理您的已保存搜索、可视化和仪表板。", "kbn.management.objects.savedObjectsSectionLabel": "已保存对象", - "management.objects.savedObjectsTitle": "已保存对象", "kbn.management.objects.view.cancelButtonAriaLabel": "取消", "kbn.management.objects.view.cancelButtonLabel": "取消", "kbn.management.objects.view.deleteItemButtonLabel": "删除“{title}”", @@ -1575,50 +1569,7 @@ "kbn.management.objects.view.viewItemTitle": "查看“{title}”", "kbn.management.savedObjects.editBreadcrumb": "编辑 {savedObjectType}", "kbn.management.savedObjects.indexBreadcrumb": "已保存对象", - "advancedSettings.advancedSettingsLabel": "高级设置", - "advancedSettings.callOutCautionDescription": "此处请谨慎操作,这些设置仅供高级用户使用。您在这里所做的更改可能使 Kibana 的大部分功能出现问题。这些设置有一部分可能未在文档中说明、不受支持或是实验性设置。如果字段有默认值,将字段留空会将其设置为默认值,其他配置指令可能不接受其默认值。删除定制设置会将其从 Kibana 的配置中永久删除。", - "advancedSettings.callOutCautionTitle": "注意:在这里您可能会使问题出现", - "advancedSettings.categoryNames.dashboardLabel": "仪表板", - "advancedSettings.categoryNames.discoverLabel": "Discover", - "advancedSettings.categoryNames.generalLabel": "常规", - "advancedSettings.categoryNames.notificationsLabel": "通知", - "advancedSettings.categoryNames.reportingLabel": "报告", - "advancedSettings.categoryNames.searchLabel": "搜索", - "advancedSettings.categoryNames.siemLabel": "SIEM", - "advancedSettings.categoryNames.timelionLabel": "Timelion", - "advancedSettings.categoryNames.visualizationsLabel": "可视化", - "advancedSettings.categorySearchLabel": "类别", - "advancedSettings.field.cancelEditingButtonAriaLabel": "取消编辑 {ariaName}", - "advancedSettings.field.cancelEditingButtonLabel": "取消", - "advancedSettings.field.changeImageLinkAriaLabel": "更改 {ariaName}", - "advancedSettings.field.changeImageLinkText": "更改图片", - "advancedSettings.field.codeEditorSyntaxErrorMessage": "JSON 语法无效", - "advancedSettings.field.customSettingAriaLabel": "定制设置", - "advancedSettings.field.customSettingTooltip": "定制设置", - "advancedSettings.field.defaultValueText": "默认值:{value}", - "advancedSettings.field.defaultValueTypeJsonText": "默认值:{value}", - "advancedSettings.field.helpText": "此设置将由 Kibana 覆盖,无法更改。", - "advancedSettings.field.imageChangeErrorMessage": "图片无法保存", - "advancedSettings.field.imageTooLargeErrorMessage": "图像过大,最大大小为 {maxSizeDescription}", - "advancedSettings.field.offLabel": "关闭", - "advancedSettings.field.onLabel": "开启", - "advancedSettings.field.requiresPageReloadToastButtonLabel": "重新加载页面", - "advancedSettings.field.requiresPageReloadToastDescription": "请重新加载页面,以使“{settingName}”设置生效。", - "advancedSettings.field.resetFieldErrorMessage": "无法重置 {name}", - "advancedSettings.field.resetToDefaultLinkAriaLabel": "将 {ariaName} 重置为默认值", - "advancedSettings.field.resetToDefaultLinkText": "重置为默认值", - "advancedSettings.field.saveButtonAriaLabel": "保存 {ariaName}", - "advancedSettings.field.saveButtonLabel": "保存", - "advancedSettings.field.saveFieldErrorMessage": "无法保存 {name}", - "advancedSettings.form.clearNoSearchResultText": "(清除搜索)", - "advancedSettings.form.clearSearchResultText": "(清除搜索)", - "advancedSettings.form.noSearchResultText": "未找到设置{clearSearch}", - "advancedSettings.form.searchResultText": "搜索词隐藏了 {settingsCount} 个设置{clearSearch}", - "advancedSettings.pageTitle": "设置", - "advancedSettings.searchBar.unableToParseQueryErrorMessage": "无法解析查询", - "advancedSettings.searchBarAriaLabel": "搜索高级设置", "kbn.managementTitle": "管理", - "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "您已搜索 {query}。{sectionLenght, plural, one {# 个部分} other {# 个部分}}中有 {optionLenght, plural, one {# 个选项} other {# 个选项}}", "kbn.topNavMenu.openInspectorButtonLabel": "检查", "kbn.topNavMenu.refreshButtonLabel": "刷新", "kbn.topNavMenu.saveVisualizationButtonLabel": "保存", @@ -1661,6 +1612,55 @@ "kbn.visualize.wizard.step1Breadcrumb": "创建", "kbn.visualize.wizard.step2Breadcrumb": "创建", "kbn.visualizeTitle": "可视化", + "advancedSettings.badge.readOnly.text": "只读", + "advancedSettings.badge.readOnly.tooltip": "无法保存高级设置", + "advancedSettings.advancedSettingsLabel": "高级设置", + "advancedSettings.callOutCautionDescription": "此处请谨慎操作,这些设置仅供高级用户使用。您在这里所做的更改可能使 Kibana 的大部分功能出现问题。这些设置有一部分可能未在文档中说明、不受支持或是实验性设置。如果字段有默认值,将字段留空会将其设置为默认值,其他配置指令可能不接受其默认值。删除定制设置会将其从 Kibana 的配置中永久删除。", + "advancedSettings.callOutCautionTitle": "注意:在这里您可能会使问题出现", + "advancedSettings.categoryNames.dashboardLabel": "仪表板", + "advancedSettings.categoryNames.discoverLabel": "Discover", + "advancedSettings.categoryNames.generalLabel": "常规", + "advancedSettings.categoryNames.notificationsLabel": "通知", + "advancedSettings.categoryNames.reportingLabel": "报告", + "advancedSettings.categoryNames.searchLabel": "搜索", + "advancedSettings.categoryNames.siemLabel": "SIEM", + "advancedSettings.categoryNames.timelionLabel": "Timelion", + "advancedSettings.categoryNames.visualizationsLabel": "可视化", + "advancedSettings.categorySearchLabel": "类别", + "advancedSettings.field.cancelEditingButtonAriaLabel": "取消编辑 {ariaName}", + "advancedSettings.field.cancelEditingButtonLabel": "取消", + "advancedSettings.field.changeImageLinkAriaLabel": "更改 {ariaName}", + "advancedSettings.field.changeImageLinkText": "更改图片", + "advancedSettings.field.codeEditorSyntaxErrorMessage": "JSON 语法无效", + "advancedSettings.field.customSettingAriaLabel": "定制设置", + "advancedSettings.field.customSettingTooltip": "定制设置", + "advancedSettings.field.defaultValueText": "默认值:{value}", + "advancedSettings.field.defaultValueTypeJsonText": "默认值:{value}", + "advancedSettings.field.helpText": "此设置将由 Kibana 覆盖,无法更改。", + "advancedSettings.field.imageChangeErrorMessage": "图片无法保存", + "advancedSettings.field.imageTooLargeErrorMessage": "图像过大,最大大小为 {maxSizeDescription}", + "advancedSettings.field.offLabel": "关闭", + "advancedSettings.field.onLabel": "开启", + "advancedSettings.field.requiresPageReloadToastButtonLabel": "重新加载页面", + "advancedSettings.field.requiresPageReloadToastDescription": "请重新加载页面,以使“{settingName}”设置生效。", + "advancedSettings.field.resetFieldErrorMessage": "无法重置 {name}", + "advancedSettings.field.resetToDefaultLinkAriaLabel": "将 {ariaName} 重置为默认值", + "advancedSettings.field.resetToDefaultLinkText": "重置为默认值", + "advancedSettings.field.saveButtonAriaLabel": "保存 {ariaName}", + "advancedSettings.field.saveButtonLabel": "保存", + "advancedSettings.field.saveFieldErrorMessage": "无法保存 {name}", + "advancedSettings.form.clearNoSearchResultText": "(清除搜索)", + "advancedSettings.form.clearSearchResultText": "(清除搜索)", + "advancedSettings.form.noSearchResultText": "未找到设置{clearSearch}", + "advancedSettings.form.searchResultText": "搜索词隐藏了 {settingsCount} 个设置{clearSearch}", + "advancedSettings.pageTitle": "设置", + "advancedSettings.searchBar.unableToParseQueryErrorMessage": "无法解析查询", + "advancedSettings.searchBarAriaLabel": "搜索高级设置", + "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "您已搜索 {query}。{sectionLenght, plural, one {# 个部分} other {# 个部分}}中有 {optionLenght, plural, one {# 个选项} other {# 个选项}}", + "management.indexPatternHeader": "索引模式", + "management.indexPatternLabel": "管理帮助从 Elasticsearch 检索数据的索引模式。", + "management.objects.savedObjectsDescription": "导入、导出和管理您的已保存搜索、可视化和仪表板。", + "management.objects.savedObjectsTitle": "已保存对象", "kibana_legacy.bigUrlWarningNotificationMessage": "在{advancedSettingsLink}中启用“{storeInSessionStorageParam}”选项或简化屏幕视觉效果。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高级设置", "kibana_legacy.bigUrlWarningNotificationTitle": "URL 过长,Kibana 可能无法工作", @@ -3914,13 +3914,6 @@ "xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigFailedTitle": "配置无法删除", "xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededText": "您已成功为“{serviceName}”删除配置。将需要一些时间才能传播到代理。", "xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededTitle": "配置已删除", - "xpack.apm.settings.agentConf.flyOut.serviceSection.alreadyConfiguredOption": "已配置", - "xpack.apm.settings.agentConf.flyOut.serviceSection.selectPlaceholder": "选择", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceEnvironmentSelectHelpText": "每个配置仅支持单个环境。", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceEnvironmentSelectLabel": "环境", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceNameSelectHelpText": "选择要配置的服务。", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceNameSelectLabel": "名称", - "xpack.apm.settings.agentConf.flyOut.serviceSection.title": "服务", "xpack.apm.settings.agentConf.flyOut.settingsSection.captureBodyInputHelpText": "有关属于 HTTP 请求的事务,代理可以选择性地捕获请求正文(例如 POST 变量)。默认为“off”。", "xpack.apm.settings.agentConf.flyOut.settingsSection.captureBodyInputLabel": "捕获正文", "xpack.apm.settings.agentConf.flyOut.settingsSection.captureBodyInputPlaceholderText": "选择选项", diff --git a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts index d8eb969b99b3b1..bda336e73c4f80 100644 --- a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts +++ b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts @@ -47,7 +47,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - await testSubjects.existOrFail('endpointManagement'); + await testSubjects.existOrFail('managementViewTitle'); }); }); diff --git a/x-pack/test/functional/apps/endpoint/index.ts b/x-pack/test/functional/apps/endpoint/index.ts index e44a4cb846f2c4..5fdf54b98cda63 100644 --- a/x-pack/test/functional/apps/endpoint/index.ts +++ b/x-pack/test/functional/apps/endpoint/index.ts @@ -11,5 +11,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./landing_page')); + loadTestFile(require.resolve('./management')); }); } diff --git a/x-pack/test/functional/apps/endpoint/management.ts b/x-pack/test/functional/apps/endpoint/management.ts new file mode 100644 index 00000000000000..bac87f34ceb82f --- /dev/null +++ b/x-pack/test/functional/apps/endpoint/management.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common', 'endpoint']); + const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); + + describe('Endpoint Management List', function() { + this.tags('ciGroup7'); + before(async () => { + await esArchiver.load('endpoint/endpoints/api_feature'); + await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/management'); + }); + + it('finds title', async () => { + const title = await testSubjects.getVisibleText('managementViewTitle'); + expect(title).to.equal('Hosts'); + }); + + it('displays table data', async () => { + const data = await pageObjects.endpoint.getManagementTableData(); + [ + 'Hostnamecadmann-4.example.com', + 'PolicyPolicy Name', + 'Policy StatusPolicy Status', + 'Alerts0', + 'Operating Systemwindows 10.0', + 'IP Address10.192.213.130, 10.70.28.129', + 'Sensor Versionversion', + 'Last Activexxxx', + ].forEach((cellValue, index) => { + expect(data[1][index]).to.equal(cellValue); + }); + }); + + after(async () => { + await esArchiver.unload('endpoint/endpoints/api_feature'); + }); + }); +}; diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts new file mode 100644 index 00000000000000..6ea9e694d29559 --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts @@ -0,0 +1,395 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../../legacy/plugins/ml/common/constants/new_job'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + const jobId = `categorization_${Date.now()}`; + const jobIdClone = `${jobId}_clone`; + const jobDescription = + 'Create categorization job based on the categorization_functional_test dataset with a count rare'; + const jobGroups = ['automated', 'categorization']; + const jobGroupsClone = [...jobGroups, 'clone']; + const detectorTypeIdentifier = 'Rare'; + const categorizationFieldIdentifier = 'field1'; + const categorizationExampleCount = 5; + const bucketSpan = '15m'; + const memoryLimit = '15mb'; + + function getExpectedRow(expectedJobId: string, expectedJobGroups: string[]) { + return { + id: expectedJobId, + description: jobDescription, + jobGroups: [...new Set(expectedJobGroups)].sort(), + recordCount: '1,501', + memoryStatus: 'ok', + jobState: 'closed', + datafeedState: 'stopped', + latestTimestamp: '2019-11-21 06:01:13', + }; + } + + function getExpectedCounts(expectedJobId: string) { + return { + job_id: expectedJobId, + processed_record_count: '1,501', + processed_field_count: '1,501', + input_bytes: '335.4 KB', + input_field_count: '1,501', + invalid_date_count: '0', + missing_field_count: '0', + out_of_order_timestamp_count: '0', + empty_bucket_count: '21,428', + sparse_bucket_count: '0', + bucket_count: '22,059', + earliest_record_timestamp: '2019-04-05 11:25:35', + latest_record_timestamp: '2019-11-21 06:01:13', + input_record_count: '1,501', + latest_bucket_timestamp: '2019-11-21 06:00:00', + latest_empty_bucket_timestamp: '2019-11-21 05:45:00', + }; + } + + function getExpectedModelSizeStats(expectedJobId: string) { + return { + job_id: expectedJobId, + result_type: 'model_size_stats', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '15.0 MB', + total_by_field_count: '30', + total_over_field_count: '0', + total_partition_field_count: '2', + bucket_allocation_failures_count: '0', + memory_status: 'ok', + timestamp: '2019-11-21 05:45:00', + }; + } + + describe('categorization', function() { + this.tags(['smoke', 'mlqa']); + before(async () => { + await esArchiver.load('ml/categorization'); + await ml.api.createCalendar('wizard-test-calendar'); + }); + + after(async () => { + await esArchiver.unload('ml/categorization'); + await ml.api.cleanMlIndices(); + }); + + it('job creation loads the job management page', async () => { + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToJobManagement(); + }); + + it('job creation loads the new job source selection page', async () => { + await ml.jobManagement.navigateToNewJobSourceSelection(); + }); + + it('job creation loads the job type selection page', async () => { + await ml.jobSourceSelection.selectSourceForAnomalyDetectionJob( + 'categorization_functional_test' + ); + }); + + it('job creation loads the categorization job wizard page', async () => { + await ml.jobTypeSelection.selectCategorizationJob(); + }); + + it('job creation displays the time range step', async () => { + await ml.jobWizardCommon.assertTimeRangeSectionExists(); + }); + + it('job creation sets the timerange', async () => { + await ml.jobWizardCommon.clickUseFullDataButton( + 'Apr 5, 2019 @ 11:25:35.770', + 'Nov 21, 2019 @ 06:01:13.914' + ); + }); + + it('job creation displays the event rate chart', async () => { + await ml.jobWizardCommon.assertEventRateChartExists(); + await ml.jobWizardCommon.assertEventRateChartHasData(); + }); + + it('job creation displays the pick fields step', async () => { + await ml.jobWizardCommon.advanceToPickFieldsSection(); + }); + + it(`job creation selects ${detectorTypeIdentifier} detector type`, async () => { + await ml.jobWizardCategorization.assertCategorizationDetectorTypeSelectionExists(); + await ml.jobWizardCategorization.selectCategorizationDetectorType(detectorTypeIdentifier); + }); + + it(`job creation selects the categorization field`, async () => { + await ml.jobWizardCategorization.assertCategorizationFieldInputExists(); + await ml.jobWizardCategorization.selectCategorizationField(categorizationFieldIdentifier); + await ml.jobWizardCategorization.assertCategorizationExamplesCallout( + CATEGORY_EXAMPLES_VALIDATION_STATUS.VALID + ); + await ml.jobWizardCategorization.assertCategorizationExamplesTable( + categorizationExampleCount + ); + }); + + it('job creation inputs the bucket span', async () => { + await ml.jobWizardCommon.assertBucketSpanInputExists(); + await ml.jobWizardCommon.setBucketSpan(bucketSpan); + }); + + it('job creation displays the job details step', async () => { + await ml.jobWizardCommon.advanceToJobDetailsSection(); + }); + + it('job creation inputs the job id', async () => { + await ml.jobWizardCommon.assertJobIdInputExists(); + await ml.jobWizardCommon.setJobId(jobId); + }); + + it('job creation inputs the job description', async () => { + await ml.jobWizardCommon.assertJobDescriptionInputExists(); + await ml.jobWizardCommon.setJobDescription(jobDescription); + }); + + it('job creation inputs job groups', async () => { + await ml.jobWizardCommon.assertJobGroupInputExists(); + for (const jobGroup of jobGroups) { + await ml.jobWizardCommon.addJobGroup(jobGroup); + } + await ml.jobWizardCommon.assertJobGroupSelection(jobGroups); + }); + + it('job creation opens the additional settings section', async () => { + await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); + }); + + it('job creation adds a new custom url', async () => { + await ml.jobWizardCommon.addCustomUrl({ label: 'check-kibana-dashboard' }); + }); + + it('job creation assigns calendars', async () => { + await ml.jobWizardCommon.addCalendar('wizard-test-calendar'); + }); + + it('job creation opens the advanced section', async () => { + await ml.jobWizardCommon.ensureAdvancedSectionOpen(); + }); + + it('job creation displays the model plot switch', async () => { + await ml.jobWizardCommon.assertModelPlotSwitchExists(); + await ml.jobWizardCommon.assertModelPlotSwitchEnabled(false); + await ml.jobWizardCommon.assertModelPlotSwitchCheckedState(false); + }); + + it('job creation enables the dedicated index switch', async () => { + await ml.jobWizardCommon.assertDedicatedIndexSwitchExists(); + await ml.jobWizardCommon.activateDedicatedIndexSwitch(); + }); + + it('job creation inputs the model memory limit', async () => { + await ml.jobWizardCommon.assertModelMemoryLimitInputExists(); + await ml.jobWizardCommon.setModelMemoryLimit(memoryLimit); + }); + + it('job creation displays the validation step', async () => { + await ml.jobWizardCommon.advanceToValidationSection(); + }); + + it('job creation displays the summary step', async () => { + await ml.jobWizardCommon.advanceToSummarySection(); + }); + + it('job creation creates the job and finishes processing', async () => { + await ml.jobWizardCommon.assertCreateJobButtonExists(); + await ml.jobWizardCommon.createJobAndWaitForCompletion(); + }); + + it('job creation displays the created job in the job list', async () => { + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToJobManagement(); + + await ml.jobTable.waitForJobsToLoad(); + await ml.jobTable.filterWithSearchString(jobId); + const rows = await ml.jobTable.parseJobTable(); + expect(rows.filter(row => row.id === jobId)).to.have.length(1); + }); + + it('job creation displays details for the created job in the job list', async () => { + await ml.jobTable.assertJobRowFields(jobId, getExpectedRow(jobId, jobGroups)); + + await ml.jobTable.assertJobRowDetailsCounts( + jobId, + getExpectedCounts(jobId), + getExpectedModelSizeStats(jobId) + ); + }); + + it('job creation has detector results', async () => { + await ml.api.assertDetectorResultsExist(jobId, 0); + }); + + it('job cloning clicks the clone action and loads the single metric wizard', async () => { + await ml.jobTable.clickCloneJobAction(jobId); + await ml.jobTypeSelection.assertCategorizationJobWizardOpen(); + }); + + it('job cloning displays the time range step', async () => { + await ml.jobWizardCommon.assertTimeRangeSectionExists(); + }); + + it('job cloning sets the timerange', async () => { + await ml.jobWizardCommon.clickUseFullDataButton( + 'Apr 5, 2019 @ 11:25:35.770', + 'Nov 21, 2019 @ 06:01:13.914' + ); + }); + + it('job cloning displays the event rate chart', async () => { + await ml.jobWizardCommon.assertEventRateChartExists(); + await ml.jobWizardCommon.assertEventRateChartHasData(); + }); + + it('job cloning displays the pick fields step', async () => { + await ml.jobWizardCommon.advanceToPickFieldsSection(); + }); + + it('job cloning pre-fills field and aggregation', async () => { + await ml.jobWizardCategorization.assertCategorizationDetectorTypeSelectionExists(); + }); + + it('job cloning pre-fills the bucket span', async () => { + await ml.jobWizardCommon.assertBucketSpanInputExists(); + await ml.jobWizardCommon.assertBucketSpanValue(bucketSpan); + }); + + it('job cloning displays the job details step', async () => { + await ml.jobWizardCommon.advanceToJobDetailsSection(); + }); + + it('job cloning does not pre-fill the job id', async () => { + await ml.jobWizardCommon.assertJobIdInputExists(); + await ml.jobWizardCommon.assertJobIdValue(''); + }); + + it('job cloning inputs the clone job id', async () => { + await ml.jobWizardCommon.setJobId(jobIdClone); + }); + + it('job cloning pre-fills the job description', async () => { + await ml.jobWizardCommon.assertJobDescriptionInputExists(); + await ml.jobWizardCommon.assertJobDescriptionValue(jobDescription); + }); + + it('job cloning pre-fills job groups', async () => { + await ml.jobWizardCommon.assertJobGroupInputExists(); + await ml.jobWizardCommon.assertJobGroupSelection(jobGroups); + }); + + it('job cloning inputs the clone job group', async () => { + await ml.jobWizardCommon.assertJobGroupInputExists(); + await ml.jobWizardCommon.addJobGroup('clone'); + await ml.jobWizardCommon.assertJobGroupSelection(jobGroupsClone); + }); + + it('job cloning opens the additional settings section', async () => { + await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); + }); + + it('job cloning persists custom urls', async () => { + await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard'); + }); + + it('job cloning persists assigned calendars', async () => { + await ml.jobWizardCommon.assertCalendarsSelection(['wizard-test-calendar']); + }); + + it('job cloning opens the advanced section', async () => { + await ml.jobWizardCommon.ensureAdvancedSectionOpen(); + }); + + it('job cloning pre-fills the model plot switch', async () => { + await ml.jobWizardCommon.assertModelPlotSwitchExists(); + await ml.jobWizardCommon.assertModelPlotSwitchEnabled(false); + await ml.jobWizardCommon.assertModelPlotSwitchCheckedState(false); + }); + + it('job cloning pre-fills the dedicated index switch', async () => { + await ml.jobWizardCommon.assertDedicatedIndexSwitchExists(); + await ml.jobWizardCommon.assertDedicatedIndexSwitchCheckedState(true); + }); + + it('job cloning pre-fills the model memory limit', async () => { + await ml.jobWizardCommon.assertModelMemoryLimitInputExists(); + await ml.jobWizardCommon.assertModelMemoryLimitValue(memoryLimit); + }); + + it('job cloning displays the validation step', async () => { + await ml.jobWizardCommon.advanceToValidationSection(); + }); + + it('job cloning displays the summary step', async () => { + await ml.jobWizardCommon.advanceToSummarySection(); + }); + + it('job cloning creates the job and finishes processing', async () => { + await ml.jobWizardCommon.assertCreateJobButtonExists(); + await ml.jobWizardCommon.createJobAndWaitForCompletion(); + }); + + it('job cloning displays the created job in the job list', async () => { + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToJobManagement(); + + await ml.jobTable.waitForJobsToLoad(); + await ml.jobTable.filterWithSearchString(jobIdClone); + const rows = await ml.jobTable.parseJobTable(); + expect(rows.filter(row => row.id === jobIdClone)).to.have.length(1); + }); + + it('job cloning displays details for the created job in the job list', async () => { + await ml.jobTable.assertJobRowFields(jobIdClone, getExpectedRow(jobIdClone, jobGroupsClone)); + + await ml.jobTable.assertJobRowDetailsCounts( + jobIdClone, + getExpectedCounts(jobIdClone), + getExpectedModelSizeStats(jobIdClone) + ); + }); + + it('job cloning has detector results', async () => { + await ml.api.assertDetectorResultsExist(jobId, 0); + }); + + it('job deletion has results for the job before deletion', async () => { + await ml.api.assertJobResultsExist(jobIdClone); + }); + + it('job deletion triggers the delete action', async () => { + await ml.jobTable.clickDeleteJobAction(jobIdClone); + }); + + it('job deletion confirms the delete modal', async () => { + await ml.jobTable.confirmDeleteJobModal(); + }); + + it('job deletion does not display the deleted job in the job list any more', async () => { + await ml.jobTable.waitForJobsToLoad(); + await ml.jobTable.filterWithSearchString(jobIdClone); + const rows = await ml.jobTable.parseJobTable(); + expect(rows.filter(row => row.id === jobIdClone)).to.have.length(0); + }); + + it('job deletion does not have results for the deleted job any more', async () => { + await ml.api.assertNoJobResultsExist(jobIdClone); + }); + }); +} diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts index a52e3d3aca2c03..28e8b221cff4ee 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts @@ -16,5 +16,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./advanced_job')); loadTestFile(require.resolve('./single_metric_viewer')); loadTestFile(require.resolve('./anomaly_explorer')); + loadTestFile(require.resolve('./categorization_job')); }); } diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index 4528a2c84d9de1..333a53a98c82ba 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -17,7 +17,8 @@ export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const transform = getService('transform'); - describe('creation_saved_search', function() { + // flaky test, see #55179 + describe.skip('creation_saved_search', function() { this.tags(['smoke']); before(async () => { await esArchiver.load('ml/farequote'); diff --git a/x-pack/test/functional/page_objects/endpoint_page.ts b/x-pack/test/functional/page_objects/endpoint_page.ts index f02a899f6d37d3..a306a855a83eb3 100644 --- a/x-pack/test/functional/page_objects/endpoint_page.ts +++ b/x-pack/test/functional/page_objects/endpoint_page.ts @@ -8,10 +8,15 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function EndpointPageProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const table = getService('table'); return { async welcomeEndpointTitle() { return await testSubjects.getVisibleText('welcomeTitle'); }, + + async getManagementTableData() { + return await table.getDataFromTestSubj('managementListTable'); + }, }; } diff --git a/x-pack/test/functional/services/machine_learning/index.ts b/x-pack/test/functional/services/machine_learning/index.ts index b01f127519670d..4cecd27631e189 100644 --- a/x-pack/test/functional/services/machine_learning/index.ts +++ b/x-pack/test/functional/services/machine_learning/index.ts @@ -21,6 +21,7 @@ export { MachineLearningJobTableProvider } from './job_table'; export { MachineLearningJobTypeSelectionProvider } from './job_type_selection'; export { MachineLearningJobWizardAdvancedProvider } from './job_wizard_advanced'; export { MachineLearningJobWizardCommonProvider } from './job_wizard_common'; +export { MachineLearningJobWizardCategorizationProvider } from './job_wizard_categorization'; export { MachineLearningJobWizardMultiMetricProvider } from './job_wizard_multi_metric'; export { MachineLearningJobWizardPopulationProvider } from './job_wizard_population'; export { MachineLearningNavigationProvider } from './navigation'; diff --git a/x-pack/test/functional/services/machine_learning/job_type_selection.ts b/x-pack/test/functional/services/machine_learning/job_type_selection.ts index 6686b5b28f200e..be66c53326a23b 100644 --- a/x-pack/test/functional/services/machine_learning/job_type_selection.ts +++ b/x-pack/test/functional/services/machine_learning/job_type_selection.ts @@ -45,5 +45,14 @@ export function MachineLearningJobTypeSelectionProvider({ getService }: FtrProvi async assertAdvancedJobWizardOpen() { await testSubjects.existOrFail('mlPageJobWizard advanced'); }, + + async selectCategorizationJob() { + await testSubjects.clickWhenNotDisabled('mlJobTypeLinkCategorizationJob'); + await this.assertCategorizationJobWizardOpen(); + }, + + async assertCategorizationJobWizardOpen() { + await testSubjects.existOrFail('mlPageJobWizard categorization'); + }, }; } diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_categorization.ts b/x-pack/test/functional/services/machine_learning/job_wizard_categorization.ts new file mode 100644 index 00000000000000..cb590c70229655 --- /dev/null +++ b/x-pack/test/functional/services/machine_learning/job_wizard_categorization.ts @@ -0,0 +1,63 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../legacy/plugins/ml/common/constants/new_job'; + +export function MachineLearningJobWizardCategorizationProvider({ getService }: FtrProviderContext) { + const comboBox = getService('comboBox'); + const testSubjects = getService('testSubjects'); + + return { + async assertCategorizationDetectorTypeSelectionExists() { + await testSubjects.existOrFail('~mlJobWizardCategorizationDetectorCountCard'); + await testSubjects.existOrFail('~mlJobWizardCategorizationDetectorRareCard'); + }, + + async selectCategorizationDetectorType(identifier: string) { + const id = `~mlJobWizardCategorizationDetector${identifier}Card`; + await testSubjects.existOrFail(id); + await testSubjects.clickWhenNotDisabled(id); + await testSubjects.existOrFail(`mlJobWizardCategorizationDetector${identifier}Card selected`); + }, + + async assertCategorizationFieldInputExists() { + await testSubjects.existOrFail('mlCategorizationFieldNameSelect > comboBoxInput'); + }, + + async selectCategorizationField(identifier: string) { + await comboBox.set('mlCategorizationFieldNameSelect > comboBoxInput', identifier); + + await this.assertCategorizationFieldSelection([identifier]); + }, + + async assertCategorizationFieldSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlCategorizationFieldNameSelect > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected categorization field selection to be '${expectedIdentifier}' (got ${comboBoxSelectedOptions}')` + ); + }, + + async assertCategorizationExamplesCallout(status: CATEGORY_EXAMPLES_VALIDATION_STATUS) { + await testSubjects.existOrFail(`mlJobWizardCategorizationExamplesCallout ${status}`); + }, + + async assertCategorizationExamplesTable(exampleCount: number) { + const table = await testSubjects.find('mlJobWizardCategorizationExamplesTable'); + const body = await table.findAllByTagName('tbody'); + expect(body.length).to.eql(1, `Expected categorization field examples table to have a body`); + const rows = await body[0].findAllByTagName('tr'); + expect(rows.length).to.eql( + exampleCount, + `Expected categorization field examples table to have '${exampleCount}' rows (got ${rows.length}')` + ); + }, + }; +} diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts index c2f408276d9e45..38e6694669c1a7 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts @@ -224,6 +224,16 @@ export function MachineLearningJobWizardCommonProvider( expect(actualCheckedState).to.eql(expectedValue); }, + async assertModelPlotSwitchEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlJobWizardSwitchModelPlot'); + expect(isEnabled).to.eql( + expectedValue, + `Expected model plot switch to be '${expectedValue ? 'enabled' : 'disabled'}' (got ${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + async assertDedicatedIndexSwitchExists( sectionOptions: SectionOptions = { withAdvancedSection: true } ) { diff --git a/x-pack/test/functional/services/ml.ts b/x-pack/test/functional/services/ml.ts index 5957a8a2eeb0ad..18574c62b84d94 100644 --- a/x-pack/test/functional/services/ml.ts +++ b/x-pack/test/functional/services/ml.ts @@ -23,6 +23,7 @@ import { MachineLearningJobTableProvider, MachineLearningJobTypeSelectionProvider, MachineLearningJobWizardAdvancedProvider, + MachineLearningJobWizardCategorizationProvider, MachineLearningJobWizardCommonProvider, MachineLearningJobWizardMultiMetricProvider, MachineLearningJobWizardPopulationProvider, @@ -49,6 +50,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const jobTable = MachineLearningJobTableProvider(context); const jobTypeSelection = MachineLearningJobTypeSelectionProvider(context); const jobWizardAdvanced = MachineLearningJobWizardAdvancedProvider(context, common); + const jobWizardCategorization = MachineLearningJobWizardCategorizationProvider(context); const jobWizardCommon = MachineLearningJobWizardCommonProvider(context, common, customUrls); const jobWizardMultiMetric = MachineLearningJobWizardMultiMetricProvider(context); const jobWizardPopulation = MachineLearningJobWizardPopulationProvider(context); @@ -73,6 +75,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { jobTable, jobTypeSelection, jobWizardAdvanced, + jobWizardCategorization, jobWizardCommon, jobWizardMultiMetric, jobWizardPopulation, diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 95371b5b501f5e..6d83e0bbf1df72 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -204,7 +204,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - it('renders the active alert instances', async () => { + it.skip('renders the active alert instances', async () => { const testBeganAt = moment().utc(); // Verify content diff --git a/yarn.lock b/yarn.lock index 1158fce12829eb..0a55e3d7c7850a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25277,6 +25277,11 @@ redux-actions@2.6.5: reduce-reducers "^0.4.3" to-camel-case "^1.0.0" +redux-devtools-extension@^2.13.8: + version "2.13.8" + resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1" + integrity sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg== + redux-observable@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-1.0.0.tgz#780ff2455493eedcef806616fe286b454fd15d91"