From 945daa4f059644fedc521be9f06692ab2db88c87 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Thu, 13 Feb 2020 17:03:37 -0700 Subject: [PATCH 01/27] [skip-ci] Remove plugin contracts header from TESTING.md (#57587) --- src/core/TESTING.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/core/TESTING.md b/src/core/TESTING.md index aac54a4a14680..9abc2bb77d7d1 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -29,7 +29,6 @@ This document outlines best practices and patterns for testing Kibana Plugins. - [Testing dependencies usages](#testing-dependencies-usages) - [Testing components consuming the dependencies](#testing-components-consuming-the-dependencies) - [Testing optional plugin dependencies](#testing-optional-plugin-dependencies) - - [Plugin Contracts](#plugin-contracts) ## Strategy @@ -1082,7 +1081,3 @@ describe('Plugin', () => { }); }); ``` - -## Plugin Contracts - -_How to test your plugin's exposed API_ From b22045433ec5dee20353624b0b23d28979796871 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 13 Feb 2020 19:03:26 -0600 Subject: [PATCH 02/27] skip flaky tests (#57643) --- .../feature_controls/advanced_settings_security.ts | 3 ++- x-pack/test/functional/apps/rollup_job/tsvb.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index 6efaae70e089b..8f902471cf6cd 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -178,7 +178,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks).to.eql(['Discover', 'Stack Management']); }); - it(`does not allow navigation to advanced settings; redirects to management home`, async () => { + // https://github.com/elastic/kibana/issues/57377 + it.skip(`does not allow navigation to advanced settings; redirects to management home`, async () => { await PageObjects.common.navigateToActualUrl('kibana', 'management/kibana/settings', { ensureCurrentUrl: false, shouldLoginIfPrompted: false, diff --git a/x-pack/test/functional/apps/rollup_job/tsvb.js b/x-pack/test/functional/apps/rollup_job/tsvb.js index f3782c4c91644..b697e751ef550 100644 --- a/x-pack/test/functional/apps/rollup_job/tsvb.js +++ b/x-pack/test/functional/apps/rollup_job/tsvb.js @@ -21,7 +21,8 @@ export default function({ getService, getPageObjects }) { 'timePicker', ]); - describe('tsvb integration', function() { + // https://github.com/elastic/kibana/issues/56816 + describe.skip('tsvb integration', function() { //Since rollups can only be created once with the same name (even if you delete it), //we add the Date.now() to avoid name collision if you run the tests locally back to back. const rollupJobName = `tsvb-test-rollup-job-${Date.now()}`; From 8513498e2d339a20727cc77e12359565580ca336 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 13 Feb 2020 22:10:27 -0600 Subject: [PATCH 03/27] [SIEM] Final Shimming/Prep for Server NP Migration (#56814) * Route all our server setup through the plugin Next we'll be trimming down ServerFacade to the final few dependencies, and using our plugin dependencies for the rest. * Clean up server plugin exports For now, let's try to simplify our typings by exporting our plugin's dependencies from the plugin itself, since we know it already knows about them. * Move DE Routes to to conventional location I'm throwing the alerting registration in the plugin for now, too. * Loosen up our RequestFacade Now that we've audited our use of request objects, we can switch to the more friendly LegacyRequest that all our utilities are expecting. LegacyRequest doesn't have plugins, either, so our only remaining errant usage is retrieving clients from it, which we call out explicitly. * Remove uses of 'src' alias This just threw an error on startup, so I'm fixing all of them to be safe. * Fix types of our GraphQL requests These come through as new KibanaRequests and not as the LegacyRequest that I had incorrectly typed them as. I'm only caring about the `body` property right now, since that's all we really deal with, and in testing it was all that was populated in our actual requests, too. * Initialize our routes with SetupServices We're using the legacy version for now, but ServerFacade will be gone by the end of the migration. * Swap legacy spaces plugin for NP This changes the signature of getIndex, which is used in quite a few places. We'll see how bad that looks in the next commit. * Remove unneeded typing We're already ignoring another portion of the platform shim due to it being incompatibly typed, so we might as well remove this. * WIP: Converting our DE routes to use consolidated services This contains our legacy stuff for now. Eventually, it will only be our NP services. This breaks a few tests due to the way createMockServer works, but I'll clean that up momentarily. * Fix DE routing tests following refactor The createMockServer helper does a few things differently: * returns mocked LegacyServices that can be passed to our route factories * does not return a server object as we don't need it, except for: * returns an inject function that we can use to execute a request We're casting our services because we're only mocking a subset of what LegacySetupServices entails. Mainly, this allows me to continue moving things off of ServerFacade without significant refactoring of tests. * Fix incompatible request types Unfortunately, LegacyRequest does not allow a request's `query` values to be anything other than a string or string[]. However, in practice they can (and are) objects, booleans, etc. Our request types (e.g. QueryRequest) are correct, but because service functions are LegacyRequest's implementation of `query`, we need to cast them lest a type error occur. For now, the easiest solution is to do this in the request handler: intersecting with our RequestFacade (LegacyRequest) allows us to both a) pluck our query params off the request and b) pass the request to NP services. * Move our use of encryptedSavedObjects to NP We're just retrieving a boolean from it right now, which is also guarded against the plugin being unavailable. If this usage becomes more widespread, we'll make this available at a higher level, probably in redux. * Use NP elasticsearch client * Simplifies our generic type to accept two arguments: params and the return value * Options is fixed and we were never specifying anything meaningful there * Updates all DE cluster calls to use callWithRequestFactory * Update DE mocks with NP elasticsearch * createMockServer now returns the callCluster mock, so that you can easily mock a client response without needing to know the details of how we define that function * Remove savedObjects dependency from our legacy dependencies This was added during a refactor, but we were never actually using this code. We always retrieve the client from the request via getSavedObjectsClient. I think that the NP client has a slightly different interface, so we're going to create a helper to retrieve it from the request, same as we do with the elastic client. In the future, both of these will be available on the request context, and we can remove the helpers entirely. * WIP: Convert services to stateful object In trying to migrate over the savedObjectsClient, I realized that it is not available during setup in the same way that the ES client is. Since our routes need pieces from both setup and start phases, I've added a Services class to accumulate/transform these services and expose scoped clients when given a legacy request. In New Platform, these clients will be available upon the request context and we should be able to remove getScopedServicesFactory for our routes. A subset of Services' functionality may still be useful, we'll see. * WIP: Converting routes to use Services factory I decided that config shouldn't live in here, as this is only client-related stuff. Probably going to rename this ClientsService. Things are still very much broken. * WIP: Qualifying our Service to ClientsService This gets us client-related services (ES, SavedObjects, Alerts), but it is independent of any configuration, which is gonna be another service. * Fix types on getIndex function This is a weird helper, I'm not really sure where it should go. * Our ClientsService is a clients ... service Return clients, as this is closer to what we'll get in the request context. * Clean up our server types * Declare legacy types at top-level file * Don't re-export from the plugin solely for convenience, that's a slippery slope straight to circular dependencies * Remove RequestFacade as it was a facade for LegacyRequest * Rename ServerFacade/LegacySetupServices to just LegacyServices * Refactor mocks for new architecture * Separates config, server, and client mocks, as they're now independent in our system, route-wise. * gets one test working, the rest will follow. * Simplify our routing mocks * Adds mock for our new clients service * Greatly simplifies both server and mock configs * Renames factory method of client service * Loosen graphQL endpoint validations These work fine in production, but it's graphQL so we don't really need the additional validation of these endpoints, and we weren't leveraging these types anywhere in Typescript land. Additionally, these restrictive validations prevent the initial introspection calls done by graphiQL to get schema information, and without schemae graphiql wasn't very helpful. This is a dev-only problem, but that's the audience of graphiql. * Remove unused graphql endpoint This was only registered in dev mode; I thought that it was needed by graphiql. However, after digging further I realized that graphiQL also only makes POST calls to our real graphQL endpoint, so this route is unnecessary. * Reduce our dependence on PluginInitializerContext After a little more introspection I realized our FrameworkAdapter doesn't need the kibana version. It was only used in order to make a dev request via (graphiql), but even that can be performed with a simpler xsrf header. This meant that we really only wanted to know whether we're in production or not, so instead we pass that simple boolean to the constructor. * Fix FrameworkAdapter type We no longer need this property. * Update detections route tests Uses the new routes interfaces, and our corresponding new mocks. * Remove unnecessary null checks Our savedObjectsClient is always going to be there. * Remove unused type YAGNI * Remove unused savedObjects client Turns out we were only destructuring this client for the null check. * Handle case where spaces is disabled We already null-coalesce properly in the clients service, but this property access was missed. * Return default signals index if spaces are disabled * Remove unnecessary casting of our alerts client mock I think that this was the result of us importing the wrong AlertsClient type, or perhaps the types were out of sync. Regardless, they work now. * Return the 'default' space even when spaces are disabled This will allow users with spaces disabled to enable spaces without losing data. The tradeoff is that they may be surprised when signals don't exist within their configured xpack.siem.signalsIndex. * Account for spaces being disabled in ClientsService * Updates types to reflect that spaces may be unavailable * Adds a test for getSpaceId's behavior when spaces are disabled * Fix false positives in query signals routes tests * Refactors mock expectations so that they're actually evaluated; they can't go within a mockImplementation call as it's evaluated in the wrong scope. * Fixes duplicated test to use the proper assertions * style: Prefer null coalescing over ternary --- x-pack/legacy/plugins/siem/index.ts | 34 ++---- x-pack/legacy/plugins/siem/server/index.ts | 2 +- .../plugins/siem/server/kibana.index.ts | 86 ------------- .../lib/alerts/elasticseatch_adapter.test.ts | 1 - .../plugins/siem/server/lib/compose/kibana.ts | 9 +- .../index/create_bootstrap_index.ts | 7 +- .../index/delete_all_index.ts | 3 +- .../detection_engine/index/delete_policy.ts | 2 +- .../detection_engine/index/delete_template.ts | 3 +- .../index/get_index_exists.ts | 1 - .../index/get_policy_exists.ts | 2 +- .../index/get_template_exists.ts | 3 +- .../lib/detection_engine/index/read_index.ts | 3 +- .../lib/detection_engine/index/set_policy.ts | 2 +- .../detection_engine/index/set_template.ts | 3 +- .../privileges/read_privileges.ts | 2 +- .../routes/__mocks__/_mock_server.ts | 113 ------------------ .../routes/__mocks__/clients_service_mock.ts | 39 ++++++ .../routes/__mocks__/index.ts | 23 ++++ .../routes/__mocks__/request_responses.ts | 9 ++ .../routes/index/create_index_route.ts | 40 ++++--- .../routes/index/delete_index_route.ts | 38 +++--- .../routes/index/read_index_route.ts | 29 +++-- .../privileges/read_privileges_route.test.ts | 33 ++--- .../privileges/read_privileges_route.ts | 29 +++-- .../rules/add_prepackaged_rules_route.test.ts | 74 +++++++----- .../rules/add_prepackaged_rules_route.ts | 53 ++++---- .../rules/create_rules_bulk_route.test.ts | 89 +++++++------- .../routes/rules/create_rules_bulk_route.ts | 43 ++++--- .../routes/rules/create_rules_route.test.ts | 102 ++++++++-------- .../routes/rules/create_rules_route.ts | 49 ++++---- .../rules/delete_rules_bulk_route.test.ts | 75 ++++++------ .../routes/rules/delete_rules_bulk_route.ts | 27 ++--- .../routes/rules/delete_rules_route.test.ts | 68 ++++++----- .../routes/rules/delete_rules_route.ts | 32 +++-- .../routes/rules/export_rules_route.ts | 24 ++-- .../routes/rules/find_rules_route.test.ts | 46 +++---- .../routes/rules/find_rules_route.ts | 25 ++-- .../routes/rules/find_rules_status_route.ts | 28 +++-- ...get_prepackaged_rules_status_route.test.ts | 63 +++++----- .../get_prepackaged_rules_status_route.ts | 19 +-- .../routes/rules/import_rules_route.ts | 59 +++++---- .../routes/rules/patch_rules_bulk.test.ts | 85 ++++++------- .../routes/rules/patch_rules_bulk_route.ts | 26 ++-- .../routes/rules/patch_rules_route.test.ts | 82 +++++++------ .../routes/rules/patch_rules_route.ts | 31 ++--- .../routes/rules/read_rules_route.test.ts | 48 ++++---- .../routes/rules/read_rules_route.ts | 27 ++--- .../routes/rules/update_rules_bulk.test.ts | 86 ++++++------- .../routes/rules/update_rules_bulk_route.ts | 32 ++--- .../routes/rules/update_rules_route.test.ts | 85 +++++++------ .../routes/rules/update_rules_route.ts | 39 +++--- .../routes/signals/open_close_signals.test.ts | 26 ++-- .../signals/open_close_signals_route.ts | 23 ++-- .../signals/query_signals_route.test.ts | 81 ++++++------- .../routes/signals/query_signals_route.ts | 23 ++-- .../routes/tags/read_tags_route.ts | 18 +-- .../lib/detection_engine/routes/utils.test.ts | 35 ++---- .../lib/detection_engine/routes/utils.ts | 22 +--- .../get_existing_prepackaged_rules.test.ts | 49 ++------ .../rules/get_export_all.test.ts | 7 +- .../rules/get_export_by_object_ids.test.ts | 16 +-- .../detection_engine/rules/read_rules.test.ts | 19 +-- .../lib/detection_engine/rules/types.ts | 26 ++-- .../lib/detection_engine/signals/types.ts | 6 +- .../detection_engine/tags/read_tags.test.ts | 61 ++-------- .../siem/server/lib/detection_engine/types.ts | 8 +- .../lib/events/elasticsearch_adapter.test.ts | 1 - .../plugins/siem/server/lib/events/mock.ts | 3 +- .../lib/framework/kibana_framework_adapter.ts | 74 ++---------- .../siem/server/lib/framework/types.ts | 21 +--- .../lib/hosts/elasticsearch_adapter.test.ts | 3 - .../plugins/siem/server/lib/hosts/mock.ts | 12 +- .../kpi_hosts/elasticsearch_adapter.test.ts | 2 - .../plugins/siem/server/lib/kpi_hosts/mock.ts | 8 +- .../lib/kpi_network/elastic_adapter.test.ts | 1 - .../siem/server/lib/kpi_network/mock.ts | 4 +- .../lib/network/elastic_adapter.test.ts | 5 - .../plugins/siem/server/lib/network/mock.ts | 9 +- .../lib/overview/elastic_adapter.test.ts | 4 - .../plugins/siem/server/lib/overview/mock.ts | 8 +- .../lib/tls/elasticsearch_adapter.test.ts | 1 - .../plugins/siem/server/lib/tls/mock.ts | 4 +- .../legacy/plugins/siem/server/lib/types.ts | 5 + x-pack/legacy/plugins/siem/server/plugin.ts | 66 ++++++++-- .../plugins/siem/server/routes/index.ts | 78 ++++++++++++ .../siem/server/services/clients.test.ts | 32 +++++ .../plugins/siem/server/services/clients.ts | 65 ++++++++++ .../plugins/siem/server/services/index.ts | 7 ++ x-pack/legacy/plugins/siem/server/types.ts | 26 +--- x-pack/plugins/siem/server/config.ts | 2 +- x-pack/plugins/siem/server/index.ts | 2 +- x-pack/plugins/siem/server/plugin.ts | 2 +- 93 files changed, 1407 insertions(+), 1391 deletions(-) delete mode 100644 x-pack/legacy/plugins/siem/server/kibana.index.ts delete mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/clients_service_mock.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.ts create mode 100644 x-pack/legacy/plugins/siem/server/routes/index.ts create mode 100644 x-pack/legacy/plugins/siem/server/services/clients.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/services/clients.ts create mode 100644 x-pack/legacy/plugins/siem/server/services/index.ts diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index 0a3e447ac64a1..c786dad61c09d 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -5,12 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { get } from 'lodash/fp'; import { resolve } from 'path'; import { Server } from 'hapi'; import { Root } from 'joi'; -import { PluginInitializerContext } from '../../../../src/core/server'; import { plugin } from './server'; import { savedObjectMappings } from './server/saved_objects'; @@ -32,7 +30,6 @@ import { SIGNALS_INDEX_KEY, } from './common/constants'; import { defaultIndexPattern } from './default_index_pattern'; -import { initServerWithKibana } from './server/kibana.index'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -151,27 +148,20 @@ export const siem = (kibana: any) => { mappings: savedObjectMappings, }, init(server: Server) { - const { config, newPlatform, plugins, route } = server; - const { coreContext, env, setup } = newPlatform; - const initializerContext = { ...coreContext, env } as PluginInitializerContext; - const serverFacade = { - config, - usingEphemeralEncryptionKey: - get('usingEphemeralEncryptionKey', newPlatform.setup.plugins.encryptedSavedObjects) ?? - false, - plugins: { - alerting: plugins.alerting, - actions: newPlatform.start.plugins.actions, - elasticsearch: plugins.elasticsearch, - spaces: plugins.spaces, - savedObjects: server.savedObjects.SavedObjectsClient, - }, - route: route.bind(server), + const { coreContext, env, setup, start } = server.newPlatform; + const initializerContext = { ...coreContext, env }; + const __legacy = { + config: server.config, + alerting: server.plugins.alerting, + route: server.route.bind(server), }; - // @ts-ignore-next-line: setup.plugins is too loosely typed - plugin(initializerContext).setup(setup.core, setup.plugins); - initServerWithKibana(initializerContext, serverFacade); + // @ts-ignore-next-line: NewPlatform shim is too loosely typed + const pluginInstance = plugin(initializerContext); + // @ts-ignore-next-line: NewPlatform shim is too loosely typed + pluginInstance.setup(setup.core, setup.plugins, __legacy); + // @ts-ignore-next-line: NewPlatform shim is too loosely typed + pluginInstance.start(start.core, start.plugins); }, config(Joi: Root) { // See x-pack/plugins/siem/server/config.ts if you're adding another diff --git a/x-pack/legacy/plugins/siem/server/index.ts b/x-pack/legacy/plugins/siem/server/index.ts index 882475390ae98..8513f871cb6c1 100644 --- a/x-pack/legacy/plugins/siem/server/index.ts +++ b/x-pack/legacy/plugins/siem/server/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext } from '../../../../../src/core/server'; import { Plugin } from './plugin'; export const plugin = (context: PluginInitializerContext) => { diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts deleted file mode 100644 index bab7936005c04..0000000000000 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ /dev/null @@ -1,86 +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 { PluginInitializerContext } from 'src/core/server'; - -import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type'; -import { createRulesRoute } from './lib/detection_engine/routes/rules/create_rules_route'; -import { createIndexRoute } from './lib/detection_engine/routes/index/create_index_route'; -import { readIndexRoute } from './lib/detection_engine/routes/index/read_index_route'; -import { readRulesRoute } from './lib/detection_engine/routes/rules/read_rules_route'; -import { findRulesRoute } from './lib/detection_engine/routes/rules/find_rules_route'; -import { deleteRulesRoute } from './lib/detection_engine/routes/rules/delete_rules_route'; -import { patchRulesRoute } from './lib/detection_engine/routes/rules/patch_rules_route'; -import { setSignalsStatusRoute } from './lib/detection_engine/routes/signals/open_close_signals_route'; -import { querySignalsRoute } from './lib/detection_engine/routes/signals/query_signals_route'; -import { ServerFacade } from './types'; -import { deleteIndexRoute } from './lib/detection_engine/routes/index/delete_index_route'; -import { isAlertExecutor } from './lib/detection_engine/signals/types'; -import { readTagsRoute } from './lib/detection_engine/routes/tags/read_tags_route'; -import { readPrivilegesRoute } from './lib/detection_engine/routes/privileges/read_privileges_route'; -import { addPrepackedRulesRoute } from './lib/detection_engine/routes/rules/add_prepackaged_rules_route'; -import { createRulesBulkRoute } from './lib/detection_engine/routes/rules/create_rules_bulk_route'; -import { patchRulesBulkRoute } from './lib/detection_engine/routes/rules/patch_rules_bulk_route'; -import { deleteRulesBulkRoute } from './lib/detection_engine/routes/rules/delete_rules_bulk_route'; -import { importRulesRoute } from './lib/detection_engine/routes/rules/import_rules_route'; -import { exportRulesRoute } from './lib/detection_engine/routes/rules/export_rules_route'; -import { findRulesStatusesRoute } from './lib/detection_engine/routes/rules/find_rules_status_route'; -import { getPrepackagedRulesStatusRoute } from './lib/detection_engine/routes/rules/get_prepackaged_rules_status_route'; -import { updateRulesRoute } from './lib/detection_engine/routes/rules/update_rules_route'; -import { updateRulesBulkRoute } from './lib/detection_engine/routes/rules/update_rules_bulk_route'; - -const APP_ID = 'siem'; - -export const initServerWithKibana = (context: PluginInitializerContext, __legacy: ServerFacade) => { - const logger = context.logger.get('plugins', APP_ID); - const version = context.env.packageInfo.version; - - if (__legacy.plugins.alerting != null) { - const type = signalRulesAlertType({ logger, version }); - if (isAlertExecutor(type)) { - __legacy.plugins.alerting.setup.registerType(type); - } - } - - // Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules - // All REST rule creation, deletion, updating, etc... - createRulesRoute(__legacy); - readRulesRoute(__legacy); - updateRulesRoute(__legacy); - deleteRulesRoute(__legacy); - findRulesRoute(__legacy); - patchRulesRoute(__legacy); - - addPrepackedRulesRoute(__legacy); - getPrepackagedRulesStatusRoute(__legacy); - createRulesBulkRoute(__legacy); - updateRulesBulkRoute(__legacy); - deleteRulesBulkRoute(__legacy); - patchRulesBulkRoute(__legacy); - - importRulesRoute(__legacy); - exportRulesRoute(__legacy); - - findRulesStatusesRoute(__legacy); - - // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals - // POST /api/detection_engine/signals/status - // Example usage can be found in siem/server/lib/detection_engine/scripts/signals - setSignalsStatusRoute(__legacy); - querySignalsRoute(__legacy); - - // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index - // All REST index creation, policy management for spaces - createIndexRoute(__legacy); - readIndexRoute(__legacy); - deleteIndexRoute(__legacy); - - // Detection Engine tags routes that have the REST endpoints of /api/detection_engine/tags - readTagsRoute(__legacy); - - // Privileges API to get the generic user privileges - readPrivilegesRoute(__legacy); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts index 3aefb6c0e1e5f..210c97892e25c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts @@ -29,7 +29,6 @@ describe('alerts elasticsearch_adapter', () => { return mockAlertsHistogramDataResponse; }); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), 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 30fdf7520a3ed..0ab6f1a8df779 100644 --- a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, PluginInitializerContext } from '../../../../../../../src/core/server'; -import { PluginsSetup } from '../../plugin'; +import { CoreSetup, SetupPlugins } from '../../plugin'; import { Anomalies } from '../anomalies'; import { ElasticsearchAnomaliesAdapter } from '../anomalies/elasticsearch_adapter'; @@ -37,10 +36,10 @@ import { Alerts, ElasticsearchAlertsAdapter } from '../alerts'; export function compose( core: CoreSetup, - plugins: PluginsSetup, - env: PluginInitializerContext['env'] + plugins: SetupPlugins, + isProductionMode: boolean ): AppBackendLibs { - const framework = new KibanaBackendFrameworkAdapter(core, plugins, env); + const framework = new KibanaBackendFrameworkAdapter(core, plugins, isProductionMode); const sources = new Sources(new ConfigurationSourcesAdapter()); const sourceStatus = new SourceStatus(new ElasticsearchSourceStatusAdapter(framework)); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts index dff6e7136bff2..253bccad2e9f8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts @@ -4,18 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; // See the reference(s) below on explanations about why -000001 was chosen and // why the is_write_index is true as well as the bootstrapping step which is needed. // Ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/applying-policy-to-template.html export const createBootstrapIndex = async ( - callWithRequest: CallWithRequest< - { path: string; method: 'PUT'; body: unknown }, - CallClusterOptions, - boolean - >, + callWithRequest: CallWithRequest<{ path: string; method: 'PUT'; body: unknown }, boolean>, index: string ): Promise => { return callWithRequest('transport.request', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts index b1d8f994615ae..d165bf69f1da1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts @@ -5,11 +5,10 @@ */ import { IndicesDeleteParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const deleteAllIndex = async ( - callWithRequest: CallWithRequest, + callWithRequest: CallWithRequest, index: string ): Promise => { return callWithRequest('indices.delete', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts index aa31c427ec84f..00213e271c7e8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts @@ -7,7 +7,7 @@ import { CallWithRequest } from '../types'; export const deletePolicy = async ( - callWithRequest: CallWithRequest<{ path: string; method: 'DELETE' }, {}, unknown>, + callWithRequest: CallWithRequest<{ path: string; method: 'DELETE' }, unknown>, policy: string ): Promise => { return callWithRequest('transport.request', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts index 63c32d13ccb8d..3402c25fb1ab1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts @@ -5,11 +5,10 @@ */ import { IndicesDeleteTemplateParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const deleteTemplate = async ( - callWithRequest: CallWithRequest, + callWithRequest: CallWithRequest, name: string ): Promise => { return callWithRequest('indices.deleteTemplate', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts index 705f542b50124..d81f23a283451 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts @@ -9,7 +9,6 @@ import { CallWithRequest } from '../types'; export const getIndexExists = async ( callWithRequest: CallWithRequest< { index: string; size: number; terminate_after: number; allow_no_indices: boolean }, - {}, { _shards: { total: number } } >, index: string diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts index d5ab1a10180c0..8a54ceac8ab78 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts @@ -7,7 +7,7 @@ import { CallWithRequest } from '../types'; export const getPolicyExists = async ( - callWithRequest: CallWithRequest<{ path: string; method: 'GET' }, {}, unknown>, + callWithRequest: CallWithRequest<{ path: string; method: 'GET' }, unknown>, policy: string ): Promise => { try { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts index fac402155619e..fd5eec8db4140 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts @@ -5,11 +5,10 @@ */ import { IndicesExistsTemplateParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const getTemplateExists = async ( - callWithRequest: CallWithRequest, + callWithRequest: CallWithRequest, template: string ): Promise => { return callWithRequest('indices.existsTemplate', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts index 0abe2b992b780..ca987f85c446c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts @@ -5,11 +5,10 @@ */ import { IndicesGetSettingsParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const readIndex = async ( - callWithRequest: CallWithRequest, + callWithRequest: CallWithRequest, index: string ): Promise => { return callWithRequest('indices.get', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts index fae28bab749ca..90d5bf9a9871b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts @@ -7,7 +7,7 @@ import { CallWithRequest } from '../types'; export const setPolicy = async ( - callWithRequest: CallWithRequest<{ path: string; method: 'PUT'; body: unknown }, {}, unknown>, + callWithRequest: CallWithRequest<{ path: string; method: 'PUT'; body: unknown }, unknown>, policy: string, body: unknown ): Promise => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts index dc9ad5dda9f7d..0894f930feffb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts @@ -5,11 +5,10 @@ */ import { IndicesPutTemplateParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const setTemplate = async ( - callWithRequest: CallWithRequest, + callWithRequest: CallWithRequest, name: string, body: unknown ): Promise => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts index a93be40738e57..01819eb4703fb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts @@ -7,7 +7,7 @@ import { CallWithRequest } from '../types'; export const readPrivileges = async ( - callWithRequest: CallWithRequest, + callWithRequest: CallWithRequest<{}, unknown>, index: string ): Promise => { return callWithRequest('transport.request', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts deleted file mode 100644 index 5b85012fd9f08..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts +++ /dev/null @@ -1,113 +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 Hapi from 'hapi'; -import { KibanaConfig } from 'src/legacy/server/kbn_server'; -import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; -import { savedObjectsClientMock } from '../../../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../../alerting/server/alerts_client.mock'; -import { actionsClientMock } from '../../../../../../../../plugins/actions/server/mocks'; -import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../../common/constants'; -import { ServerFacade } from '../../../../types'; - -const defaultConfig = { - 'kibana.index': '.kibana', - [`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`]: '.siem-signals', -}; - -const isKibanaConfig = (config: unknown): config is KibanaConfig => - Object.getOwnPropertyDescriptor(config, 'get') != null && - Object.getOwnPropertyDescriptor(config, 'has') != null; - -const assertNever = (): never => { - throw new Error('Unexpected object'); -}; - -const createMockKibanaConfig = (config: Record): KibanaConfig => { - const returnConfig = { - get(key: string) { - return config[key]; - }, - has(key: string) { - return config[key] != null; - }, - }; - if (isKibanaConfig(returnConfig)) { - return returnConfig; - } else { - return assertNever(); - } -}; - -export const createMockServer = (config: Record = defaultConfig) => { - const server = new Hapi.Server({ - port: 0, - }); - - server.config = () => createMockKibanaConfig(config); - - const actionsClient = actionsClientMock.create(); - const alertsClient = alertsClientMock.create(); - const savedObjectsClient = savedObjectsClientMock.create(); - const elasticsearch = { - getCluster: jest.fn().mockImplementation(() => ({ - callWithRequest: jest.fn(), - })), - }; - server.decorate('request', 'getAlertsClient', () => alertsClient); - server.plugins.elasticsearch = (elasticsearch as unknown) as ElasticsearchPlugin; - server.plugins.spaces = { getSpaceId: () => 'default' }; - server.plugins.actions = { - getActionsClientWithRequest: () => actionsClient, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; // The types have really bad conflicts at the moment so I have to use any - server.decorate('request', 'getSavedObjectsClient', () => savedObjectsClient); - return { - server: server as ServerFacade & Hapi.Server, - alertsClient, - actionsClient, - elasticsearch, - savedObjectsClient, - }; -}; - -export const createMockServerWithoutAlertClientDecoration = ( - config: Record = defaultConfig -) => { - const serverWithoutAlertClient = new Hapi.Server({ - port: 0, - }); - - const savedObjectsClient = savedObjectsClientMock.create(); - serverWithoutAlertClient.config = () => createMockKibanaConfig(config); - serverWithoutAlertClient.decorate('request', 'getSavedObjectsClient', () => savedObjectsClient); - serverWithoutAlertClient.plugins.actions = { - getActionsClientWithRequest: () => actionsClient, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; // The types have really bad conflicts at the moment so I have to use any - - const actionsClient = actionsClientMock.create(); - - return { - serverWithoutAlertClient: serverWithoutAlertClient as ServerFacade & Hapi.Server, - actionsClient, - }; -}; - -export const getMockIndexName = () => - jest.fn().mockImplementation(() => ({ - callWithRequest: jest.fn().mockImplementationOnce(() => 'index-name'), - })); - -export const getMockEmptyIndex = () => - jest.fn().mockImplementation(() => ({ - callWithRequest: jest.fn().mockImplementation(() => ({ _shards: { total: 0 } })), - })); - -export const getMockNonEmptyIndex = () => - jest.fn().mockImplementation(() => ({ - callWithRequest: jest.fn().mockImplementation(() => ({ _shards: { total: 1 } })), - })); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/clients_service_mock.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/clients_service_mock.ts new file mode 100644 index 0000000000000..f89e938b8a636 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/clients_service_mock.ts @@ -0,0 +1,39 @@ +/* + * 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 { + elasticsearchServiceMock, + savedObjectsClientMock, +} from '../../../../../../../../../src/core/server/mocks'; +import { alertsClientMock } from '../../../../../../alerting/server/alerts_client.mock'; +import { ActionsClient } from '../../../../../../../../plugins/actions/server'; +import { actionsClientMock } from '../../../../../../../../plugins/actions/server/mocks'; +import { GetScopedClients } from '../../../../services'; + +const createClients = () => ({ + actionsClient: actionsClientMock.create() as jest.Mocked, + alertsClient: alertsClientMock.create(), + clusterClient: elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClient: savedObjectsClientMock.create(), + spacesClient: { getSpaceId: jest.fn() }, +}); + +const createGetScoped = () => + jest.fn(() => Promise.resolve(createClients()) as ReturnType); + +const createClientsServiceMock = () => { + return { + setup: jest.fn(), + start: jest.fn(), + createGetScoped, + }; +}; + +export const clientsServiceMock = { + create: createClientsServiceMock, + createGetScoped, + createClients, +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.ts new file mode 100644 index 0000000000000..250b006814294 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.ts @@ -0,0 +1,23 @@ +/* + * 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 Hapi from 'hapi'; + +export { clientsServiceMock } from './clients_service_mock'; + +export const createMockServer = () => { + const server = new Hapi.Server({ port: 0 }); + + return { + route: server.route.bind(server), + inject: server.inject.bind(server), + }; +}; + +export const createMockConfig = () => () => ({ + get: jest.fn(), + has: jest.fn(), +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index b008ead8df948..f380b82c1e05f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -17,6 +17,7 @@ import { INTERNAL_IMMUTABLE_KEY, DETECTION_ENGINE_PREPACKAGED_URL, } from '../../../../../common/constants'; +import { ShardsResponse } from '../../../types'; import { RuleAlertType, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { RuleAlertParamsRest, PrepackagedRules } from '../../types'; @@ -413,3 +414,11 @@ export const getFindResultStatus = (): SavedObjectsFindResponse 'index-name'; +export const getEmptyIndex = (): { _shards: Partial } => ({ + _shards: { total: 0 }, +}); +export const getNonEmptyIndex = (): { _shards: Partial } => ({ + _shards: { total: 1 }, +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts index e0d48836013ec..2502009a2e6a2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts @@ -7,9 +7,9 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; -import signalsPolicy from './signals_policy.json'; -import { ServerFacade, RequestFacade } from '../../../../types'; -import { transformError, getIndex, callWithRequestFactory } from '../utils'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; +import { transformError, getIndex } from '../utils'; import { getIndexExists } from '../../index/get_index_exists'; import { getPolicyExists } from '../../index/get_policy_exists'; import { setPolicy } from '../../index/set_policy'; @@ -17,8 +17,12 @@ import { setTemplate } from '../../index/set_template'; import { getSignalsTemplate } from './get_signals_template'; import { getTemplateExists } from '../../index/get_template_exists'; import { createBootstrapIndex } from '../../index/create_bootstrap_index'; +import signalsPolicy from './signals_policy.json'; -export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createCreateIndexRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: DETECTION_ENGINE_INDEX_URL, @@ -30,11 +34,13 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }, }, }, - async handler(request: RequestFacade, headers) { + async handler(request: LegacyRequest, headers) { try { - const index = getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, index); + const { clusterClient, spacesClient } = await getClients(request); + const callCluster = clusterClient.callAsCurrentUser; + + const index = getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists(callCluster, index); if (indexExists) { return headers .response({ @@ -43,16 +49,16 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }) .code(409); } else { - const policyExists = await getPolicyExists(callWithRequest, index); + const policyExists = await getPolicyExists(callCluster, index); if (!policyExists) { - await setPolicy(callWithRequest, index, signalsPolicy); + await setPolicy(callCluster, index, signalsPolicy); } - const templateExists = await getTemplateExists(callWithRequest, index); + const templateExists = await getTemplateExists(callCluster, index); if (!templateExists) { const template = getSignalsTemplate(index); - await setTemplate(callWithRequest, index, template); + await setTemplate(callCluster, index, template); } - await createBootstrapIndex(callWithRequest, index); + await createBootstrapIndex(callCluster, index); return { acknowledged: true }; } } catch (err) { @@ -68,6 +74,10 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const createIndexRoute = (server: ServerFacade) => { - server.route(createCreateIndexRoute(server)); +export const createIndexRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(createCreateIndexRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts index c1edc824b81eb..ae61afb6f8d06 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts @@ -7,8 +7,9 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../../types'; -import { transformError, getIndex, callWithRequestFactory } from '../utils'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; +import { transformError, getIndex } from '../utils'; import { getIndexExists } from '../../index/get_index_exists'; import { getPolicyExists } from '../../index/get_policy_exists'; import { deletePolicy } from '../../index/delete_policy'; @@ -26,7 +27,10 @@ import { deleteTemplate } from '../../index/delete_template'; * * And ensuring they're all gone */ -export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createDeleteIndexRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'DELETE', path: DETECTION_ENGINE_INDEX_URL, @@ -38,11 +42,13 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }, }, }, - async handler(request: RequestFacade, headers) { + async handler(request: LegacyRequest, headers) { try { - const index = getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, index); + const { clusterClient, spacesClient } = await getClients(request); + const callCluster = clusterClient.callAsCurrentUser; + + const index = getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists(callCluster, index); if (!indexExists) { return headers .response({ @@ -51,14 +57,14 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }) .code(404); } else { - await deleteAllIndex(callWithRequest, `${index}-*`); - const policyExists = await getPolicyExists(callWithRequest, index); + await deleteAllIndex(callCluster, `${index}-*`); + const policyExists = await getPolicyExists(callCluster, index); if (policyExists) { - await deletePolicy(callWithRequest, index); + await deletePolicy(callCluster, index); } - const templateExists = await getTemplateExists(callWithRequest, index); + const templateExists = await getTemplateExists(callCluster, index); if (templateExists) { - await deleteTemplate(callWithRequest, index); + await deleteTemplate(callCluster, index); } return { acknowledged: true }; } @@ -75,6 +81,10 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const deleteIndexRoute = (server: ServerFacade) => { - server.route(createDeleteIndexRoute(server)); +export const deleteIndexRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(createDeleteIndexRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts index 1a5018d446747..41be42f7c0fe1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts @@ -7,11 +7,15 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../../types'; -import { transformError, getIndex, callWithRequestFactory } from '../utils'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; +import { transformError, getIndex } from '../utils'; import { getIndexExists } from '../../index/get_index_exists'; -export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createReadIndexRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'GET', path: DETECTION_ENGINE_INDEX_URL, @@ -23,11 +27,14 @@ export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute => }, }, }, - async handler(request: RequestFacade, headers) { + async handler(request: LegacyRequest, headers) { try { - const index = getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, index); + const { clusterClient, spacesClient } = await getClients(request); + const callCluster = clusterClient.callAsCurrentUser; + + const index = getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists(callCluster, index); + if (indexExists) { // head request is used for if you want to get if the index exists // or not and it will return a content-length: 0 along with either a 200 or 404 @@ -62,6 +69,10 @@ export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute => }; }; -export const readIndexRoute = (server: ServerFacade) => { - server.route(createReadIndexRoute(server)); +export const readIndexRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(createReadIndexRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts index 1ea681afb7949..308ee95a77e20 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts @@ -4,35 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMockServer } from '../__mocks__/_mock_server'; -import { getPrivilegeRequest, getMockPrivileges } from '../__mocks__/request_responses'; import { readPrivilegesRoute } from './read_privileges_route'; -import * as myUtils from '../utils'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; +import { getPrivilegeRequest, getMockPrivileges } from '../__mocks__/request_responses'; describe('read_privileges', () => { - let { server, elasticsearch } = createMockServer(); + let { route, inject } = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { - jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex'); - ({ server, elasticsearch } = createMockServer()); - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn(() => getMockPrivileges()), - })); - readPrivilegesRoute(server); - }); - - afterEach(() => { jest.resetAllMocks(); + ({ route, inject } = createMockServer()); + + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getMockPrivileges()); + + readPrivilegesRoute(route, config, false, getClients); }); describe('normal status codes', () => { test('returns 200 when doing a normal request', async () => { - const { statusCode } = await server.inject(getPrivilegeRequest()); + const { statusCode } = await inject(getPrivilegeRequest()); expect(statusCode).toBe(200); }); test('returns the payload when doing a normal request', async () => { - const { payload } = await server.inject(getPrivilegeRequest()); + const { payload } = await inject(getPrivilegeRequest()); expect(JSON.parse(payload)).toEqual(getMockPrivileges()); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 45ecb7dc97288..e9b9bffbaf054 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -6,13 +6,19 @@ import Hapi from 'hapi'; import { merge } from 'lodash/fp'; + import { DETECTION_ENGINE_PRIVILEGES_URL } from '../../../../../common/constants'; +import { LegacyServices } from '../../../../types'; import { RulesRequest } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; -import { callWithRequestFactory, transformError, getIndex } from '../utils'; +import { GetScopedClients } from '../../../../services'; +import { transformError, getIndex } from '../utils'; import { readPrivileges } from '../../privileges/read_privileges'; -export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createReadPrivilegesRulesRoute = ( + config: LegacyServices['config'], + usingEphemeralEncryptionKey: boolean, + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'GET', path: DETECTION_ENGINE_PRIVILEGES_URL, @@ -26,10 +32,10 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve }, async handler(request: RulesRequest, headers) { try { - const callWithRequest = callWithRequestFactory(request, server); - const index = getIndex(request, server); - const permissions = await readPrivileges(callWithRequest, index); - const usingEphemeralEncryptionKey = server.usingEphemeralEncryptionKey; + const { clusterClient, spacesClient } = await getClients(request); + + const index = getIndex(spacesClient.getSpaceId, config); + const permissions = await readPrivileges(clusterClient.callAsCurrentUser, index); return merge(permissions, { is_authenticated: request?.auth?.isAuthenticated ?? false, has_encryption_key: !usingEphemeralEncryptionKey, @@ -47,6 +53,11 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve }; }; -export const readPrivilegesRoute = (server: ServerFacade): void => { - server.route(createReadPrivilegesRulesRoute(server)); +export const readPrivilegesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + usingEphemeralEncryptionKey: boolean, + getClients: GetScopedClients +) => { + route(createReadPrivilegesRulesRoute(config, usingEphemeralEncryptionKey, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index ec86de84ff3c7..e018ed4cc22ff 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -4,12 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, - getMockEmptyIndex, - getMockNonEmptyIndex, -} from '../__mocks__/_mock_server'; +import { omit } from 'lodash/fp'; + import { createRulesRoute } from './create_rules_route'; import { getFindResult, @@ -17,7 +13,10 @@ import { createActionResult, addPrepackagedRulesRequest, getFindResultWithSingleHit, + getEmptyIndex, + getNonEmptyIndex, } from '../__mocks__/request_responses'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -48,45 +47,56 @@ import { addPrepackedRulesRoute } from './add_prepackaged_rules_route'; import { PrepackagedRules } from '../../types'; describe('add_prepackaged_rules_route', () => { - let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); - elasticsearch.getCluster = getMockNonEmptyIndex(); - addPrepackedRulesRoute(server); + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); + + addPrepackedRulesRoute(server.route, config, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when creating a with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { statusCode } = await server.inject(addPrepackagedRulesRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(addPrepackagedRulesRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { inject, route } = createMockServer(); + createRulesRoute(route, config, getClients); + const { statusCode } = await inject(addPrepackagedRulesRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('it returns a 400 if the index does not exist', async () => { - elasticsearch.getCluster = getMockEmptyIndex(); - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(addPrepackagedRulesRequest()); expect(JSON.parse(payload)).toEqual({ - message: - 'Pre-packaged rules cannot be installed until the space index is created: .siem-signals-default', + message: expect.stringContaining( + 'Pre-packaged rules cannot be installed until the space index is created' + ), status_code: 400, }); }); @@ -94,10 +104,10 @@ describe('add_prepackaged_rules_route', () => { describe('payload', () => { test('1 rule is installed and 0 are updated when find results are empty', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(addPrepackagedRulesRequest()); expect(JSON.parse(payload)).toEqual({ rules_installed: 1, @@ -106,10 +116,10 @@ describe('add_prepackaged_rules_route', () => { }); test('1 rule is updated and 0 are installed when we return a single find and the versions are different', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(addPrepackagedRulesRequest()); expect(JSON.parse(payload)).toEqual({ rules_installed: 0, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index e796f21d9c03a..c4d0489486ef8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -5,21 +5,23 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { getIndexExists } from '../../index/get_index_exists'; -import { callWithRequestFactory, getIndex, transformError } from '../utils'; +import { getIndex, transformError } from '../utils'; import { getPrepackagedRules } from '../../rules/get_prepackaged_rules'; import { installPrepackagedRules } from '../../rules/install_prepacked_rules'; import { updatePrepackagedRules } from '../../rules/update_prepacked_rules'; import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createAddPrepackedRulesRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'PUT', path: DETECTION_ENGINE_PREPACKAGED_URL, @@ -31,29 +33,32 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR }, }, }, - async handler(request: RequestFacade, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } - + async handler(request: LegacyRequest, headers) { try { - const callWithRequest = callWithRequestFactory(request, server); + const { + actionsClient, + alertsClient, + clusterClient, + savedObjectsClient, + spacesClient, + } = await getClients(request); + + if (!actionsClient || !alertsClient) { + return headers.response().code(404); + } + const rulesFromFileSystem = getPrepackagedRules(); const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules); - const spaceIndex = getIndex(request, server); + const spaceIndex = getIndex(spacesClient.getSpaceId, config); if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) { - const spaceIndexExists = await getIndexExists(callWithRequest, spaceIndex); + const spaceIndexExists = await getIndexExists( + clusterClient.callAsCurrentUser, + spaceIndex + ); if (!spaceIndexExists) { return headers .response({ @@ -90,6 +95,10 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR }; }; -export const addPrepackedRulesRoute = (server: ServerFacade): void => { - server.route(createAddPrepackedRulesRoute(server)); +export const addPrepackedRulesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createAddPrepackedRulesRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index f1169442484c6..664d27a7572ad 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -4,59 +4,66 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, - getMockEmptyIndex, -} from '../__mocks__/_mock_server'; -import { createRulesRoute } from './create_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; + import { getFindResult, getResult, createActionResult, typicalPayload, getReadBulkRequest, + getEmptyIndex, } from '../__mocks__/request_responses'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRulesBulkRoute } from './create_rules_bulk_route'; import { BulkError } from '../utils'; import { OutputRuleAlertRest } from '../../types'; describe('create_rules_bulk', () => { - let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); - createRulesBulkRoute(server); + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + getClients.mockResolvedValue(clients); + + createRulesBulkRoute(server.route, config, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getReadBulkRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getReadBulkRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { inject, route } = createMockServer(); + createRulesBulkRoute(route, config, getClients); + const { statusCode } = await inject(getReadBulkRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('it gets a 409 if the index does not exist', async () => { - elasticsearch.getCluster = getMockEmptyIndex(); - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(getReadBulkRequest()); expect(JSON.parse(payload)).toEqual([ { @@ -71,10 +78,10 @@ describe('create_rules_bulk', () => { }); test('returns 200 if rule_id is not given as the id is auto generated from the alert framework', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); // missing rule_id should return 200 as it will be auto generated if not given const { rule_id, ...noRuleId } = typicalPayload(); const request: ServerInjectOptions = { @@ -87,10 +94,10 @@ describe('create_rules_bulk', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', @@ -107,10 +114,10 @@ describe('create_rules_bulk', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', @@ -128,10 +135,10 @@ describe('create_rules_bulk', () => { }); test('returns 409 if duplicate rule_ids found in request payload', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'POST', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, @@ -143,10 +150,10 @@ describe('create_rules_bulk', () => { }); test('returns one error object in response when duplicate rule_ids found in request payload', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'POST', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index e7145d2a6f055..51b7b132fc794 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -5,25 +5,24 @@ */ import Hapi from 'hapi'; -import { isFunction, countBy } from 'lodash/fp'; +import { countBy } from 'lodash/fp'; import uuid from 'uuid'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { GetScopedClients } from '../../../../services'; +import { LegacyServices } from '../../../../types'; import { createRules } from '../../rules/create_rules'; import { BulkRulesRequest } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; import { readRules } from '../../rules/read_rules'; import { transformOrBulkError, getDuplicates } from './utils'; import { getIndexExists } from '../../index/get_index_exists'; -import { - callWithRequestFactory, - getIndex, - transformBulkError, - createBulkErrorObject, -} from '../utils'; +import { getIndex, transformBulkError, createBulkErrorObject } from '../utils'; import { createRulesBulkSchema } from '../schemas/create_rules_bulk_schema'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createCreateRulesBulkRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, @@ -37,14 +36,11 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }, }, async handler(request: BulkRulesRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) + const { actionsClient, alertsClient, clusterClient, spacesClient } = await getClients( + request ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + + if (!actionsClient || !alertsClient) { return headers.response().code(404); } @@ -85,9 +81,8 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou } = payloadRule; const ruleIdOrUuid = ruleId ?? uuid.v4(); try { - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, finalIndex); + const finalIndex = outputIndex ?? getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); if (!indexExists) { return createBulkErrorObject({ ruleId: ruleIdOrUuid, @@ -155,6 +150,10 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }; }; -export const createRulesBulkRoute = (server: ServerFacade): void => { - server.route(createCreateRulesBulkRoute(server)); +export const createRulesBulkRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createCreateRulesBulkRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index e51634c0d2c07..4f28771db8ed7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, - getMockNonEmptyIndex, - getMockEmptyIndex, -} from '../__mocks__/_mock_server'; -import { createRulesRoute } from './create_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { createRulesRoute } from './create_rules_route'; import { getFindResult, @@ -20,57 +17,58 @@ import { getCreateRequest, typicalPayload, getFindResultStatus, + getNonEmptyIndex, + getEmptyIndex, } from '../__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; describe('create_rules', () => { - let { - server, - alertsClient, - actionsClient, - elasticsearch, - savedObjectsClient, - } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ - server, - alertsClient, - actionsClient, - elasticsearch, - savedObjectsClient, - } = createMockServer()); - elasticsearch.getCluster = getMockNonEmptyIndex(); - createRulesRoute(server); + + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); + + createRulesRoute(server.route, config, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getCreateRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getCreateRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + createRulesRoute(route, config, getClients); + const { statusCode } = await inject(getCreateRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('it returns a 400 if the index does not exist', async () => { - elasticsearch.getCluster = getMockEmptyIndex(); - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(getCreateRequest()); expect(JSON.parse(payload)).toEqual({ message: 'To create a rule, the index must exist first. Index .siem-signals does not exist', @@ -79,11 +77,11 @@ describe('create_rules', () => { }); test('returns 200 if rule_id is not given as the id is auto generated from the alert framework', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); // missing rule_id should return 200 as it will be auto generated if not given const { rule_id, ...noRuleId } = typicalPayload(); const request: ServerInjectOptions = { @@ -96,11 +94,11 @@ describe('create_rules', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', @@ -115,11 +113,11 @@ describe('create_rules', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index de874f66d0444..19e772165628d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -5,21 +5,24 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import uuid from 'uuid'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { GetScopedClients } from '../../../../services'; +import { LegacyServices } from '../../../../types'; import { createRules } from '../../rules/create_rules'; import { RulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { createRulesSchema } from '../schemas/create_rules_schema'; -import { ServerFacade } from '../../../../types'; import { readRules } from '../../rules/read_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { transform } from './utils'; import { getIndexExists } from '../../index/get_index_exists'; -import { callWithRequestFactory, getIndex, transformError } from '../utils'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; +import { getIndex, transformError } from '../utils'; -export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createCreateRulesRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: DETECTION_ENGINE_RULES_URL, @@ -59,21 +62,21 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = type, references, } = request.payload; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } - try { - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, finalIndex); + const { + alertsClient, + actionsClient, + clusterClient, + savedObjectsClient, + spacesClient, + } = await getClients(request); + + if (!actionsClient || !alertsClient) { + return headers.response().code(404); + } + + const finalIndex = outputIndex ?? getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); if (!indexExists) { return headers .response({ @@ -157,6 +160,10 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const createRulesRoute = (server: ServerFacade): void => { - server.route(createCreateRulesRoute(server)); +export const createRulesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createCreateRulesRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts index e66fc765c08bf..855bf7f634c26 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts @@ -4,10 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; +import { omit } from 'lodash/fp'; import { ServerInjectOptions } from 'hapi'; import { @@ -20,70 +17,75 @@ import { getDeleteAsPostBulkRequestById, getFindResultStatus, } from '../__mocks__/request_responses'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { deleteRulesBulkRoute } from './delete_rules_bulk_route'; import { BulkError } from '../utils'; describe('delete_rules', () => { - let { server, alertsClient, savedObjectsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { - ({ server, alertsClient, savedObjectsClient } = createMockServer()); - deleteRulesBulkRoute(server); - }); - - afterEach(() => { jest.resetAllMocks(); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + deleteRulesBulkRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteBulkRequest()); expect(statusCode).toBe(200); }); test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId using POST', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteAsPostBulkRequest()); expect(statusCode).toBe(200); }); test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteBulkRequestById()); expect(statusCode).toBe(200); }); test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id using POST', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteAsPostBulkRequestById()); expect(statusCode).toBe(200); }); test('returns 200 because the error is in the payload when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteBulkRequest()); expect(statusCode).toBe(200); }); test('returns 404 in the payload when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); - savedObjectsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); const { payload } = await server.inject(getDeleteBulkRequest()); const parsed: BulkError[] = JSON.parse(payload); const expected: BulkError[] = [ @@ -96,18 +98,19 @@ describe('delete_rules', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - deleteRulesBulkRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getDeleteBulkRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + deleteRulesBulkRoute(route, getClients); + const { statusCode } = await inject(getDeleteBulkRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if given a non-existent id in the payload', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const request: ServerInjectOptions = { method: 'DELETE', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index b3f8eafa24115..6438318cb43db 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -5,19 +5,18 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { deleteRules } from '../../rules/delete_rules'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { queryRulesBulkSchema } from '../schemas/query_rules_bulk_schema'; import { transformOrBulkError, getIdBulkError } from './utils'; import { transformBulkError } from '../utils'; import { QueryBulkRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; +import { deleteRules } from '../../rules/delete_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createDeleteRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createDeleteRulesBulkRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { method: ['POST', 'DELETE'], // allow both POST and DELETE in case their client does not support bodies in DELETE path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, @@ -31,14 +30,9 @@ export const createDeleteRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }, }, async handler(request: QueryBulkRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + const { actionsClient, alertsClient, savedObjectsClient } = await getClients(request); + + if (!actionsClient || !alertsClient) { return headers.response().code(404); } const rules = await Promise.all( @@ -78,6 +72,9 @@ export const createDeleteRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }; }; -export const deleteRulesBulkRoute = (server: ServerFacade): void => { - server.route(createDeleteRulesBulkRoute(server)); +export const deleteRulesBulkRoute = ( + route: LegacyServices['route'], + getClients: GetScopedClients +): void => { + route(createDeleteRulesBulkRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index 0aa60d3bbd922..a0a6f61223279 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { deleteRulesRoute } from './delete_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; +import { deleteRulesRoute } from './delete_rules_route'; import { getFindResult, @@ -20,64 +16,70 @@ import { getDeleteRequestById, getFindResultStatus, } from '../__mocks__/request_responses'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('delete_rules', () => { - let { server, alertsClient, savedObjectsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { - ({ server, alertsClient, savedObjectsClient } = createMockServer()); - deleteRulesRoute(server); - }); - - afterEach(() => { jest.resetAllMocks(); + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + + deleteRulesRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); - savedObjectsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteRequest()); expect(statusCode).toBe(200); }); test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); - savedObjectsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteRequestById()); expect(statusCode).toBe(200); }); test('returns 404 when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); - savedObjectsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - deleteRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getDeleteRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + deleteRulesRoute(route, getClients); + const { statusCode } = await inject(getDeleteRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if given a non-existent id', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const request: ServerInjectOptions = { method: 'DELETE', url: DETECTION_ENGINE_RULES_URL, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts index e4d3787c90072..340782523b724 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -5,19 +5,18 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { deleteRules } from '../../rules/delete_rules'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { queryRulesSchema } from '../schemas/query_rules_schema'; import { getIdError, transform } from './utils'; import { transformError } from '../utils'; import { QueryRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createDeleteRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createDeleteRulesRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { method: 'DELETE', path: DETECTION_ENGINE_RULES_URL, @@ -30,20 +29,16 @@ export const createDeleteRulesRoute = (server: ServerFacade): Hapi.ServerRoute = query: queryRulesSchema, }, }, - async handler(request: QueryRequest, headers) { + async handler(request: QueryRequest & LegacyRequest, headers) { const { id, rule_id: ruleId } = request.query; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } try { + const { actionsClient, alertsClient, savedObjectsClient } = await getClients(request); + + if (!actionsClient || !alertsClient) { + return headers.response().code(404); + } + const rule = await deleteRules({ actionsClient, alertsClient, @@ -95,6 +90,9 @@ export const createDeleteRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const deleteRulesRoute = (server: ServerFacade): void => { - server.route(createDeleteRulesRoute(server)); +export const deleteRulesRoute = ( + route: LegacyServices['route'], + getClients: GetScopedClients +): void => { + route(createDeleteRulesRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts index 5da5ffcd58bf1..1966b06701803 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -5,17 +5,21 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { ExportRulesRequest } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_rules'; import { exportRulesSchema, exportRulesQuerySchema } from '../schemas/export_rules_schema'; import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; import { getExportAll } from '../../rules/get_export_all'; import { transformError } from '../utils'; -export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createExportRulesRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: `${DETECTION_ENGINE_RULES_URL}/_export`, @@ -29,15 +33,15 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = query: exportRulesQuerySchema, }, }, - async handler(request: ExportRulesRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + async handler(request: ExportRulesRequest & LegacyRequest, headers) { + const { alertsClient } = await getClients(request); if (!alertsClient) { return headers.response().code(404); } try { - const exportSizeLimit = server.config().get('savedObjects.maxImportExportSize'); + const exportSizeLimit = config().get('savedObjects.maxImportExportSize'); if (request.payload?.objects != null && request.payload.objects.length > exportSizeLimit) { return headers .response({ @@ -82,6 +86,10 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const exportRulesRoute = (server: ServerFacade): void => { - server.route(createExportRulesRoute(server)); +export const exportRulesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createExportRulesRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts index 62c9f44da1e33..5b75f17164acf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; +import { omit } from 'lodash/fp'; + +import { createMockServer } from '../__mocks__'; +import { clientsServiceMock } from '../__mocks__/clients_service_mock'; import { findRulesRoute } from './find_rules_route'; import { ServerInjectOptions } from 'hapi'; @@ -16,43 +16,49 @@ import { getFindResult, getResult, getFindRequest } from '../__mocks__/request_r import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('find_rules', () => { - let { server, alertsClient, actionsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { - ({ server, alertsClient, actionsClient } = createMockServer()); - findRulesRoute(server); - }); - - afterEach(() => { jest.resetAllMocks(); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + + findRulesRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when finding a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.find.mockResolvedValue({ + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.find.mockResolvedValue({ page: 1, perPage: 1, total: 0, data: [], }); - alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getFindRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - findRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getFindRequest()); + const { route, inject } = createMockServer(); + getClients.mockResolvedValue(omit('alertsClient', clients)); + findRulesRoute(route, getClients); + const { statusCode } = await inject(getFindRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if a bad query parameter is given', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'GET', url: `${DETECTION_ENGINE_RULES_URL}/_find?invalid_value=500`, @@ -62,8 +68,8 @@ describe('find_rules', () => { }); test('returns 200 if the set of optional query parameters are given', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'GET', url: `${DETECTION_ENGINE_RULES_URL}/_find?page=2&per_page=20&sort_field=timestamp&fields=["field-1","field-2","field-3]`, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts index b15c1db7222cf..4297e4aebfd58 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -5,17 +5,17 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { findRules } from '../../rules/find_rules'; import { FindRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { findRulesSchema } from '../schemas/find_rules_schema'; -import { ServerFacade } from '../../../../types'; import { transformFindAlerts } from './utils'; import { transformError } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -export const createFindRulesRoute = (): Hapi.ServerRoute => { +export const createFindRulesRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { method: 'GET', path: `${DETECTION_ENGINE_RULES_URL}/_find`, @@ -28,17 +28,14 @@ export const createFindRulesRoute = (): Hapi.ServerRoute => { query: findRulesSchema, }, }, - async handler(request: FindRulesRequest, headers) { + async handler(request: FindRulesRequest & LegacyRequest, headers) { const { query } = request; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } - try { + const { alertsClient, savedObjectsClient } = await getClients(request); + if (!alertsClient) { + return headers.response().code(404); + } + const rules = await findRules({ alertsClient, perPage: query.per_page, @@ -86,6 +83,6 @@ export const createFindRulesRoute = (): Hapi.ServerRoute => { }; }; -export const findRulesRoute = (server: ServerFacade) => { - server.route(createFindRulesRoute()); +export const findRulesRoute = (route: LegacyServices['route'], getClients: GetScopedClients) => { + route(createFindRulesRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index 8b3113a044b5a..fe8742ff0b60c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -5,10 +5,11 @@ */ import Hapi from 'hapi'; -import { isFunction, snakeCase } from 'lodash/fp'; +import { snakeCase } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { findRulesStatusesSchema } from '../schemas/find_rules_statuses_schema'; import { FindRulesStatusesRequest, @@ -29,7 +30,7 @@ const convertToSnakeCase = >(obj: T): Partial | }, {}); }; -export const createFindRulesStatusRoute: Hapi.ServerRoute = { +export const createFindRulesStatusRoute = (getClients: GetScopedClients): Hapi.ServerRoute => ({ method: 'GET', path: `${DETECTION_ENGINE_RULES_URL}/_find_statuses`, options: { @@ -41,19 +42,17 @@ export const createFindRulesStatusRoute: Hapi.ServerRoute = { query: findRulesStatusesSchema, }, }, - async handler(request: FindRulesStatusesRequest, headers) { + async handler(request: FindRulesStatusesRequest & LegacyRequest, headers) { const { query } = request; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + const { alertsClient, savedObjectsClient } = await getClients(request); + + if (!alertsClient) { return headers.response().code(404); } // build return object with ids as keys and errors as values. /* looks like this - { + { "someAlertId": [{"myerrorobject": "some error value"}, etc..], "anotherAlertId": ... } @@ -86,8 +85,11 @@ export const createFindRulesStatusRoute: Hapi.ServerRoute = { }, Promise.resolve({})); return statuses; }, -}; +}); -export const findRulesStatusesRoute = (server: ServerFacade): void => { - server.route(createFindRulesStatusRoute); +export const findRulesStatusesRoute = ( + route: LegacyServices['route'], + getClients: GetScopedClients +): void => { + route(createFindRulesStatusRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts index de7f0fe26cc74..8f27910a7e5e2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts @@ -4,19 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, - getMockNonEmptyIndex, -} from '../__mocks__/_mock_server'; -import { createRulesRoute } from './create_rules_route'; +import { omit } from 'lodash/fp'; + +import { getPrepackagedRulesStatusRoute } from './get_prepackaged_rules_status_route'; + import { getFindResult, getResult, createActionResult, getFindResultWithSingleHit, getPrepackagedRulesStatusRequest, + getNonEmptyIndex, } from '../__mocks__/request_responses'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -41,44 +41,49 @@ jest.mock('../../rules/get_prepackaged_rules', () => { }; }); -import { getPrepackagedRulesStatusRoute } from './get_prepackaged_rules_status_route'; - describe('get_prepackaged_rule_status_route', () => { - let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); - elasticsearch.getCluster = getMockNonEmptyIndex(); - getPrepackagedRulesStatusRoute(server); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); + + getPrepackagedRulesStatusRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when creating a with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getPrepackagedRulesStatusRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject( - getPrepackagedRulesStatusRequest() - ); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + getPrepackagedRulesStatusRoute(route, getClients); + const { statusCode } = await inject(getPrepackagedRulesStatusRequest()); expect(statusCode).toBe(404); }); }); describe('payload', () => { test('0 rules installed, 0 custom rules, 1 rules not installed, and 1 rule not updated', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(getPrepackagedRulesStatusRequest()); expect(JSON.parse(payload)).toEqual({ rules_custom_installed: 0, @@ -89,10 +94,10 @@ describe('get_prepackaged_rule_status_route', () => { }); test('1 rule installed, 1 custom rules, 0 rules not installed, and 1 rule to not updated', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(getPrepackagedRulesStatusRequest()); expect(JSON.parse(payload)).toEqual({ rules_custom_installed: 1, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index c999292ba7674..bee57d6b38127 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -5,10 +5,10 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { transformError } from '../utils'; import { getPrepackagedRules } from '../../rules/get_prepackaged_rules'; import { getRulesToInstall } from '../../rules/get_rules_to_install'; @@ -16,7 +16,9 @@ import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { findRules } from '../../rules/find_rules'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => { +export const createGetPrepackagedRulesStatusRoute = ( + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'GET', path: `${DETECTION_ENGINE_PREPACKAGED_URL}/_status`, @@ -28,8 +30,8 @@ export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => { }, }, }, - async handler(request: RequestFacade, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + async handler(request: LegacyRequest, headers) { + const { alertsClient } = await getClients(request); if (!alertsClient) { return headers.response().code(404); @@ -67,6 +69,9 @@ export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => { }; }; -export const getPrepackagedRulesStatusRoute = (server: ServerFacade): void => { - server.route(createGetPrepackagedRulesStatusRoute()); +export const getPrepackagedRulesStatusRoute = ( + route: LegacyServices['route'], + getClients: GetScopedClients +): void => { + route(createGetPrepackagedRulesStatusRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 5e87c99d815ef..a9188cc2adc12 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -5,40 +5,39 @@ */ import Hapi from 'hapi'; -import { chunk, isEmpty, isFunction } from 'lodash/fp'; +import { chunk, isEmpty } from 'lodash/fp'; import { extname } from 'path'; import uuid from 'uuid'; + import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { LegacyServices, LegacyRequest } from '../../../../types'; import { createRules } from '../../rules/create_rules'; import { ImportRulesRequest } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; import { readRules } from '../../rules/read_rules'; import { getIndexExists } from '../../index/get_index_exists'; -import { - callWithRequestFactory, - getIndex, - createBulkErrorObject, - ImportRuleResponse, -} from '../utils'; +import { getIndex, createBulkErrorObject, ImportRuleResponse } from '../utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { ImportRuleAlertRest } from '../../types'; import { patchRules } from '../../rules/patch_rules'; import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; +import { GetScopedClients } from '../../../../services'; type PromiseFromStreams = ImportRuleAlertRest | Error; const CHUNK_PARSED_OBJECT_SIZE = 10; -export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createImportRulesRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: `${DETECTION_ENGINE_RULES_URL}/_import`, options: { tags: ['access:siem'], payload: { - maxBytes: server.config().get('savedObjects.maxImportPayloadBytes'), + maxBytes: config().get('savedObjects.maxImportPayloadBytes'), output: 'stream', allow: 'multipart/form-data', }, @@ -50,17 +49,19 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = payload: importRulesPayloadSchema, }, }, - async handler(request: ImportRulesRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + async handler(request: ImportRulesRequest & LegacyRequest, headers) { + const { + actionsClient, + alertsClient, + clusterClient, + spacesClient, + savedObjectsClient, + } = await getClients(request); + + if (!actionsClient || !alertsClient) { return headers.response().code(404); } + const { filename } = request.payload.file.hapi; const fileExtension = extname(filename).toLowerCase(); if (fileExtension !== '.ndjson') { @@ -72,7 +73,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = .code(400); } - const objectLimit = server.config().get('savedObjects.maxImportExportSize'); + const objectLimit = config().get('savedObjects.maxImportExportSize'); const readStream = createRulesStreamFromNdJson(request.payload.file, objectLimit); const parsedObjects = await createPromiseFromStreams([readStream]); const uniqueParsedObjects = Array.from( @@ -146,9 +147,11 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = version, } = parsedRule; try { - const finalIndex = getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, finalIndex); + const finalIndex = getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists( + clusterClient.callAsCurrentUser, + finalIndex + ); if (!indexExists) { resolve( createBulkErrorObject({ @@ -261,6 +264,10 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const importRulesRoute = (server: ServerFacade): void => { - server.route(createImportRulesRoute(server)); +export const importRulesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createImportRulesRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts index aa0dd04786a2e..02af4135b534f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { patchRulesRoute } from './patch_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { patchRulesRoute } from './patch_rules_route'; +import { omit } from 'lodash/fp'; import { getFindResult, @@ -20,43 +16,51 @@ import { getFindResultWithSingleHit, getPatchBulkRequest, } from '../__mocks__/request_responses'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { patchRulesBulkRoute } from './patch_rules_bulk_route'; import { BulkError } from '../utils'; describe('patch_rules_bulk', () => { - let { server, alertsClient, actionsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient } = createMockServer()); - patchRulesBulkRoute(server); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + patchRulesBulkRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getPatchBulkRequest()); expect(statusCode).toBe(200); }); test('returns 200 as a response when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getPatchBulkRequest()); expect(statusCode).toBe(200); }); test('returns 404 within the payload when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { payload } = await server.inject(getPatchBulkRequest()); const parsed: BulkError[] = JSON.parse(payload); const expected: BulkError[] = [ @@ -69,17 +73,18 @@ describe('patch_rules_bulk', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - patchRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getPatchBulkRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + patchRulesRoute(route, getClients); + const { statusCode } = await inject(getPatchBulkRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if id is not given in either the body or the url', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); const { rule_id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'PATCH', @@ -91,9 +96,9 @@ describe('patch_rules_bulk', () => { }); test('returns errors as 200 to just indicate ok something happened', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PATCH', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -104,9 +109,9 @@ describe('patch_rules_bulk', () => { }); test('returns 404 in the payload if the record does not exist yet', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PATCH', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -124,10 +129,10 @@ describe('patch_rules_bulk', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PATCH', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -138,10 +143,10 @@ describe('patch_rules_bulk', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PATCH', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 00184b6c16b7e..d3f92e9e05bcc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -5,21 +5,21 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { BulkPatchRulesRequest, IRuleSavedAttributesSavedObjectAttributes, } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { transformOrBulkError, getIdBulkError } from './utils'; import { transformBulkError } from '../utils'; import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema'; import { patchRules } from '../../rules/patch_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createPatchRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createPatchRulesBulkRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { method: 'PATCH', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -33,14 +33,9 @@ export const createPatchRulesBulkRoute = (server: ServerFacade): Hapi.ServerRout }, }, async handler(request: BulkPatchRulesRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + const { actionsClient, alertsClient, savedObjectsClient } = await getClients(request); + + if (!actionsClient || !alertsClient) { return headers.response().code(404); } @@ -132,6 +127,9 @@ export const createPatchRulesBulkRoute = (server: ServerFacade): Hapi.ServerRout }; }; -export const patchRulesBulkRoute = (server: ServerFacade): void => { - server.route(createPatchRulesBulkRoute(server)); +export const patchRulesBulkRoute = ( + route: LegacyServices['route'], + getClients: GetScopedClients +): void => { + route(createPatchRulesBulkRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index d315d45046e2d..cc84b08fdef11 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { patchRulesRoute } from './patch_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; +import { patchRulesRoute } from './patch_rules_route'; import { getFindResult, @@ -21,51 +17,59 @@ import { typicalPayload, getFindResultWithSingleHit, } from '../__mocks__/request_responses'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('patch_rules', () => { - let { server, alertsClient, actionsClient, savedObjectsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient, savedObjectsClient } = createMockServer()); - patchRulesRoute(server); + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + patchRulesRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getPatchRequest()); expect(statusCode).toBe(200); }); test('returns 404 when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getPatchRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - patchRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getPatchRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + patchRulesRoute(route, getClients); + const { statusCode } = await inject(getPatchRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if id is not given in either the body or the url', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { rule_id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'PATCH', @@ -79,10 +83,10 @@ describe('patch_rules', () => { }); test('returns 404 if the record does not exist yet', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const request: ServerInjectOptions = { method: 'PATCH', url: DETECTION_ENGINE_RULES_URL, @@ -93,11 +97,11 @@ describe('patch_rules', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const request: ServerInjectOptions = { method: 'PATCH', url: DETECTION_ENGINE_RULES_URL, @@ -108,11 +112,11 @@ describe('patch_rules', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PATCH', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts index e27ae81362f27..761d22b084237 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -5,18 +5,18 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { patchRules } from '../../rules/patch_rules'; import { PatchRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { patchRulesSchema } from '../schemas/patch_rules_schema'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { getIdError, transform } from './utils'; import { transformError } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createPatchRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createPatchRulesRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { method: 'PATCH', path: DETECTION_ENGINE_RULES_URL, @@ -59,21 +59,16 @@ export const createPatchRulesRoute = (server: ServerFacade): Hapi.ServerRoute => version, } = request.payload; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } - try { + const { alertsClient, actionsClient, savedObjectsClient } = await getClients(request); + + if (!actionsClient || !alertsClient) { + return headers.response().code(404); + } + const rule = await patchRules({ - alertsClient, actionsClient, + alertsClient, description, enabled, falsePositives, @@ -146,6 +141,6 @@ export const createPatchRulesRoute = (server: ServerFacade): Hapi.ServerRoute => }; }; -export const patchRulesRoute = (server: ServerFacade) => { - server.route(createPatchRulesRoute(server)); +export const patchRulesRoute = (route: LegacyServices['route'], getClients: GetScopedClients) => { + route(createPatchRulesRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index 000cd29af8ba9..7c4653af97f21 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { readRulesRoute } from './read_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { readRulesRoute } from './read_rules_route'; import { getFindResult, getResult, @@ -19,43 +16,48 @@ import { getFindResultWithSingleHit, getFindResultStatus, } from '../__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; describe('read_signals', () => { - let { server, alertsClient, savedObjectsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { - ({ server, alertsClient, savedObjectsClient } = createMockServer()); - readRulesRoute(server); - }); - - afterEach(() => { jest.resetAllMocks(); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + readRulesRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when reading a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getReadRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - readRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getReadRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + readRulesRoute(route, getClients); + const { statusCode } = await inject(getReadRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if given a non-existent id', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const request: ServerInjectOptions = { method: 'GET', url: DETECTION_ENGINE_RULES_URL, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts index e82ad92704695..0180b208d1bb7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -5,18 +5,18 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { getIdError, transform } from './utils'; import { transformError } from '../utils'; import { readRules } from '../../rules/read_rules'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; import { queryRulesSchema } from '../schemas/query_rules_schema'; import { QueryRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { GetScopedClients } from '../../../../services'; -export const createReadRulesRoute: Hapi.ServerRoute = { +export const createReadRulesRoute = (getClients: GetScopedClients): Hapi.ServerRoute => ({ method: 'GET', path: DETECTION_ENGINE_RULES_URL, options: { @@ -28,16 +28,15 @@ export const createReadRulesRoute: Hapi.ServerRoute = { query: queryRulesSchema, }, }, - async handler(request: QueryRequest, headers) { + async handler(request: QueryRequest & LegacyRequest, headers) { const { id, rule_id: ruleId } = request.query; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } + try { + const { alertsClient, savedObjectsClient } = await getClients(request); + if (!alertsClient) { + return headers.response().code(404); + } + const rule = await readRules({ alertsClient, id, @@ -84,8 +83,8 @@ export const createReadRulesRoute: Hapi.ServerRoute = { .code(error.statusCode); } }, -}; +}); -export const readRulesRoute = (server: ServerFacade) => { - server.route(createReadRulesRoute); +export const readRulesRoute = (route: LegacyServices['route'], getClients: GetScopedClients) => { + route(createReadRulesRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts index 81b6444f38603..9ff7ebc37aab1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts @@ -4,14 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { updateRulesRoute } from './update_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; +import { updateRulesRoute } from './update_rules_route'; import { getFindResult, getResult, @@ -20,43 +16,52 @@ import { getFindResultWithSingleHit, getUpdateBulkRequest, } from '../__mocks__/request_responses'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { updateRulesBulkRoute } from './update_rules_bulk_route'; import { BulkError } from '../utils'; describe('update_rules_bulk', () => { - let { server, alertsClient, actionsClient } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient } = createMockServer()); - updateRulesBulkRoute(server); + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + updateRulesBulkRoute(server.route, config, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getUpdateBulkRequest()); expect(statusCode).toBe(200); }); test('returns 200 as a response when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getUpdateBulkRequest()); expect(statusCode).toBe(200); }); test('returns 404 within the payload when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { payload } = await server.inject(getUpdateBulkRequest()); const parsed: BulkError[] = JSON.parse(payload); const expected: BulkError[] = [ @@ -69,17 +74,18 @@ describe('update_rules_bulk', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - updateRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getUpdateBulkRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + updateRulesRoute(route, config, getClients); + const { statusCode } = await inject(getUpdateBulkRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if id is not given in either the body or the url', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); const { rule_id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', @@ -91,9 +97,9 @@ describe('update_rules_bulk', () => { }); test('returns errors as 200 to just indicate ok something happened', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PUT', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -104,9 +110,9 @@ describe('update_rules_bulk', () => { }); test('returns 404 in the payload if the record does not exist yet', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PUT', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -124,10 +130,10 @@ describe('update_rules_bulk', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PUT', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -138,10 +144,10 @@ describe('update_rules_bulk', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 671497f9f65db..98ed01474c3dc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -5,21 +5,24 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { BulkUpdateRulesRequest, IRuleSavedAttributesSavedObjectAttributes, } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { transformOrBulkError, getIdBulkError } from './utils'; import { transformBulkError, getIndex } from '../utils'; import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; import { updateRules } from '../../rules/update_rules'; -export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createUpdateRulesBulkRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'PUT', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -33,14 +36,11 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }, }, async handler(request: BulkUpdateRulesRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) + const { actionsClient, alertsClient, savedObjectsClient, spacesClient } = await getClients( + request ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + + if (!actionsClient || !alertsClient) { return headers.response().code(404); } @@ -74,7 +74,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou references, version, } = payloadRule; - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); + const finalIndex = outputIndex ?? getIndex(spacesClient.getSpaceId, config); const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; try { const rule = await updateRules({ @@ -134,6 +134,10 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }; }; -export const updateRulesBulkRoute = (server: ServerFacade): void => { - server.route(createUpdateRulesBulkRoute(server)); +export const updateRulesBulkRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createUpdateRulesBulkRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index c4f10d7a20327..7cadfa94467a7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -4,14 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { updateRulesRoute } from './update_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; +import { updateRulesRoute } from './update_rules_route'; import { getFindResult, getFindResultStatus, @@ -21,51 +17,62 @@ import { typicalPayload, getFindResultWithSingleHit, } from '../__mocks__/request_responses'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('update_rules', () => { - let { server, alertsClient, actionsClient, savedObjectsClient } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient, savedObjectsClient } = createMockServer()); - updateRulesRoute(server); + + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + updateRulesRoute(server.route, config, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getUpdateRequest()); expect(statusCode).toBe(200); }); test('returns 404 when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - updateRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getUpdateRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + updateRulesRoute(route, config, getClients); + const { statusCode } = await inject(getUpdateRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if id is not given in either the body or the url', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { rule_id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', @@ -79,10 +86,10 @@ describe('update_rules', () => { }); test('returns 404 if the record does not exist yet', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const request: ServerInjectOptions = { method: 'PUT', url: DETECTION_ENGINE_RULES_URL, @@ -93,11 +100,11 @@ describe('update_rules', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const request: ServerInjectOptions = { method: 'PUT', url: DETECTION_ENGINE_RULES_URL, @@ -108,11 +115,11 @@ describe('update_rules', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index a01627d2094b7..80fdfc1df8e0e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -5,18 +5,20 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { UpdateRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { updateRulesSchema } from '../schemas/update_rules_schema'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { getIdError, transform } from './utils'; import { transformError, getIndex } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; import { updateRules } from '../../rules/update_rules'; -export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createUpdateRulesRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'PUT', path: DETECTION_ENGINE_RULES_URL, @@ -59,19 +61,16 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = version, } = request.payload; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } - try { - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); + const { alertsClient, actionsClient, savedObjectsClient, spacesClient } = await getClients( + request + ); + + if (!actionsClient || !alertsClient) { + return headers.response().code(404); + } + + const finalIndex = outputIndex ?? getIndex(spacesClient.getSpaceId, config); const rule = await updateRules({ alertsClient, actionsClient, @@ -148,6 +147,10 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const updateRulesRoute = (server: ServerFacade) => { - server.route(createUpdateRulesRoute(server)); +export const updateRulesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(createUpdateRulesRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts index 35e1e5933af64..3e7ed4de6d8c6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMockServer } from '../__mocks__/_mock_server'; +import { ServerInjectOptions } from 'hapi'; +import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; import { setSignalsStatusRoute } from './open_close_signals_route'; import * as myUtils from '../utils'; -import { ServerInjectOptions } from 'hapi'; import { getSetSignalStatusByIdsRequest, @@ -16,19 +16,27 @@ import { typicalSetStatusSignalByQueryPayload, setStatusSignalMissingIdsAndQueryPayload, } from '../__mocks__/request_responses'; -import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; describe('set signal status', () => { - let { server, elasticsearch } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex'); - ({ server, elasticsearch } = createMockServer()); - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn(() => true), - })); - setSignalsStatusRoute(server); + + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(true); + + setSignalsStatusRoute(server.route, config, getClients); }); describe('status on signal', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index 4755869c3d908..b2b938625180e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -6,12 +6,16 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { SignalsStatusRequest } from '../../signals/types'; import { setSignalsStatusSchema } from '../schemas/set_signal_status_schema'; -import { ServerFacade } from '../../../../types'; import { transformError, getIndex } from '../utils'; -export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute => { +export const setSignalsStatusRouteDef = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: DETECTION_ENGINE_SIGNALS_STATUS_URL, @@ -26,8 +30,9 @@ export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute }, async handler(request: SignalsStatusRequest) { const { signal_ids: signalIds, query, status } = request.payload; - const index = getIndex(request, server); - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); + const { clusterClient, spacesClient } = await getClients(request); + const index = getIndex(spacesClient.getSpaceId, config); + let queryObject; if (signalIds) { queryObject = { ids: { values: signalIds } }; @@ -40,7 +45,7 @@ export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute }; } try { - return callWithRequest(request, 'updateByQuery', { + return clusterClient.callAsCurrentUser('updateByQuery', { index, body: { script: { @@ -58,6 +63,10 @@ export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute }; }; -export const setSignalsStatusRoute = (server: ServerFacade) => { - server.route(setSignalsStatusRouteDef(server)); +export const setSignalsStatusRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(setSignalsStatusRouteDef(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts index 5b86d0a4b36c0..9439adfcec3cb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts @@ -4,77 +4,78 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMockServer } from '../__mocks__/_mock_server'; -import { querySignalsRoute } from './query_signals_route'; -import * as myUtils from '../utils'; import { ServerInjectOptions } from 'hapi'; +import { querySignalsRoute } from './query_signals_route'; +import * as myUtils from '../utils'; import { getSignalsQueryRequest, getSignalsAggsQueryRequest, typicalSignalsQuery, typicalSignalsQueryAggs, } from '../__mocks__/request_responses'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; describe('query for signal', () => { - let { server, elasticsearch } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex'); - ({ server, elasticsearch } = createMockServer()); - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn(() => true), - })); - querySignalsRoute(server); + + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(true); + + querySignalsRoute(server.route, config, getClients); }); describe('query and agg on signals index', () => { test('returns 200 when using single query', async () => { - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn( - (_req, _type: string, queryBody: { index: string; body: object }) => { - expect(queryBody.body).toMatchObject({ ...typicalSignalsQueryAggs() }); - return true; - } - ), - })); - const { statusCode } = await server.inject(getSignalsAggsQueryRequest()); + const { statusCode } = await server.inject(getSignalsQueryRequest()); + expect(statusCode).toBe(200); + expect(clients.clusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'search', + expect.objectContaining({ body: typicalSignalsQuery() }) + ); expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); }); test('returns 200 when using single agg', async () => { - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn( - (_req, _type: string, queryBody: { index: string; body: object }) => { - expect(queryBody.body).toMatchObject({ ...typicalSignalsQueryAggs() }); - return true; - } - ), - })); const { statusCode } = await server.inject(getSignalsAggsQueryRequest()); + expect(statusCode).toBe(200); + expect(clients.clusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'search', + expect.objectContaining({ body: typicalSignalsQueryAggs() }) + ); expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); }); test('returns 200 when using aggs and query together', async () => { - const allTogether = getSignalsQueryRequest(); - allTogether.payload = { ...typicalSignalsQueryAggs(), ...typicalSignalsQuery() }; - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn( - (_req, _type: string, queryBody: { index: string; body: object }) => { - expect(queryBody.body).toMatchObject({ - ...typicalSignalsQueryAggs(), - ...typicalSignalsQuery(), - }); - return true; - } - ), - })); - const { statusCode } = await server.inject(allTogether); + const request = getSignalsQueryRequest(); + request.payload = { ...typicalSignalsQueryAggs(), ...typicalSignalsQuery() }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + expect(clients.clusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'search', + expect.objectContaining({ + body: { + ...typicalSignalsQuery(), + ...typicalSignalsQueryAggs(), + }, + }) + ); expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts index 6d1896b1a8171..a3602ffbded41 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -6,12 +6,16 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { SignalsQueryRequest } from '../../signals/types'; import { querySignalsSchema } from '../schemas/query_signals_index_schema'; -import { ServerFacade } from '../../../../types'; import { transformError, getIndex } from '../utils'; -export const querySignalsRouteDef = (server: ServerFacade): Hapi.ServerRoute => { +export const querySignalsRouteDef = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: DETECTION_ENGINE_QUERY_SIGNALS_URL, @@ -26,11 +30,12 @@ export const querySignalsRouteDef = (server: ServerFacade): Hapi.ServerRoute => }, async handler(request: SignalsQueryRequest) { const { query, aggs, _source, track_total_hits, size } = request.payload; - const index = getIndex(request, server); - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); + const { clusterClient, spacesClient } = await getClients(request); + + const index = getIndex(spacesClient.getSpaceId, config); try { - return callWithRequest(request, 'search', { + return clusterClient.callAsCurrentUser('search', { index, body: { query, aggs, _source, track_total_hits, size }, }); @@ -42,6 +47,10 @@ export const querySignalsRouteDef = (server: ServerFacade): Hapi.ServerRoute => }; }; -export const querySignalsRoute = (server: ServerFacade) => { - server.route(querySignalsRouteDef(server)); +export const querySignalsRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(querySignalsRouteDef(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts index f6d297b0cbf43..b17be21d15a19 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts @@ -5,13 +5,14 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; + import { DETECTION_ENGINE_TAGS_URL } from '../../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; import { transformError } from '../utils'; import { readTags } from '../../tags/read_tags'; +import { GetScopedClients } from '../../../../services'; -export const createReadTagsRoute: Hapi.ServerRoute = { +export const createReadTagsRoute = (getClients: GetScopedClients): Hapi.ServerRoute => ({ method: 'GET', path: DETECTION_ENGINE_TAGS_URL, options: { @@ -22,8 +23,9 @@ export const createReadTagsRoute: Hapi.ServerRoute = { }, }, }, - async handler(request: RequestFacade, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + async handler(request: LegacyRequest, headers) { + const { alertsClient } = await getClients(request); + if (!alertsClient) { return headers.response().code(404); } @@ -43,8 +45,8 @@ export const createReadTagsRoute: Hapi.ServerRoute = { .code(error.statusCode); } }, -}; +}); -export const readTagsRoute = (server: ServerFacade) => { - server.route(createReadTagsRoute); +export const readTagsRoute = (route: LegacyServices['route'], getClients: GetScopedClients) => { + route(createReadTagsRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 2699f687c5106..957ddd4ee6caa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -16,6 +16,7 @@ import { createImportErrorObject, transformImportError, } from './utils'; +import { createMockConfig } from './__mocks__'; describe('utils', () => { describe('transformError', () => { @@ -295,34 +296,20 @@ describe('utils', () => { }); describe('getIndex', () => { - it('appends the space ID to the configured index if spaces are enabled', () => { - const mockGet = jest.fn(); - const mockGetSpaceId = jest.fn(); - const config = jest.fn(() => ({ get: mockGet, has: jest.fn() })); - const server = { plugins: { spaces: { getSpaceId: mockGetSpaceId } }, config }; + let mockConfig = createMockConfig(); - mockGet.mockReturnValue('mockSignalsIndex'); - mockGetSpaceId.mockReturnValue('myspace'); - // @ts-ignore-next-line TODO these dependencies are simplified on - // https://github.com/elastic/kibana/pull/56814. We're currently mocking - // out what we need. - const index = getIndex(null, server); - - expect(index).toEqual('mockSignalsIndex-myspace'); + beforeEach(() => { + mockConfig = () => ({ + get: jest.fn(() => 'mockSignalsIndex'), + has: jest.fn(), + }); }); - it('appends the default space ID to the configured index if spaces are disabled', () => { - const mockGet = jest.fn(); - const config = jest.fn(() => ({ get: mockGet, has: jest.fn() })); - const server = { plugins: {}, config }; + it('appends the space id to the configured index', () => { + const getSpaceId = jest.fn(() => 'myspace'); + const index = getIndex(getSpaceId, mockConfig); - mockGet.mockReturnValue('mockSignalsIndex'); - // @ts-ignore-next-line TODO these dependencies are simplified on - // https://github.com/elastic/kibana/pull/56814. We're currently mocking - // out what we need. - const index = getIndex(null, server); - - expect(index).toEqual('mockSignalsIndex-default'); + expect(index).toEqual('mockSignalsIndex-myspace'); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 20871e5309c30..4a586e21c9e7f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../types'; +import { LegacyServices } from '../../../types'; export interface OutputError { message: string; @@ -174,21 +174,9 @@ export const transformBulkError = ( } }; -export const getIndex = ( - request: RequestFacade | Omit, - server: ServerFacade -): string => { - const spaceId = server.plugins.spaces?.getSpaceId?.(request) ?? 'default'; - const signalsIndex = server.config().get(`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`); - return `${signalsIndex}-${spaceId}`; -}; +export const getIndex = (getSpaceId: () => string, config: LegacyServices['config']): string => { + const signalsIndex = config().get(`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`); + const spaceId = getSpaceId(); -export const callWithRequestFactory = ( - request: RequestFacade | Omit, - server: ServerFacade -) => { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - return (endpoint: string, params: T, options?: U) => { - return callWithRequest(request, endpoint, params, options); - }; + return `${signalsIndex}-${spaceId}`; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts index 8d00ddb18be6b..25bac383ecf72 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts @@ -5,7 +5,6 @@ */ import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; -import { AlertsClient } from '../../../../../alerting'; import { getResult, getFindResultWithSingleHit, @@ -28,10 +27,7 @@ describe('get_existing_prepackaged_rules', () => { test('should return a single item in a single page', async () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getExistingPrepackagedRules({ - alertsClient: unsafeCast, - }); + const rules = await getExistingPrepackagedRules({ alertsClient }); expect(rules).toEqual([getResult()]); }); @@ -70,10 +66,7 @@ describe('get_existing_prepackaged_rules', () => { }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getExistingPrepackagedRules({ - alertsClient: unsafeCast, - }); + const rules = await getExistingPrepackagedRules({ alertsClient }); expect(rules).toEqual([result1, result2, result3]); }); }); @@ -82,10 +75,7 @@ describe('get_existing_prepackaged_rules', () => { test('should return a single item in a single page', async () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRules({ - alertsClient: unsafeCast, - }); + const rules = await getNonPackagedRules({ alertsClient }); expect(rules).toEqual([getResult()]); }); @@ -113,10 +103,7 @@ describe('get_existing_prepackaged_rules', () => { getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRules({ - alertsClient: unsafeCast, - }); + const rules = await getNonPackagedRules({ alertsClient }); expect(rules).toEqual([result1, result2]); }); @@ -152,10 +139,7 @@ describe('get_existing_prepackaged_rules', () => { }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRules({ - alertsClient: unsafeCast, - }); + const rules = await getNonPackagedRules({ alertsClient }); expect(rules).toEqual([result1, result2, result3]); }); }); @@ -164,11 +148,7 @@ describe('get_existing_prepackaged_rules', () => { test('should return a single item in a single page', async () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRules({ - alertsClient: unsafeCast, - filter: '', - }); + const rules = await getRules({ alertsClient, filter: '' }); expect(rules).toEqual([getResult()]); }); @@ -196,11 +176,7 @@ describe('get_existing_prepackaged_rules', () => { getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRules({ - alertsClient: unsafeCast, - filter: '', - }); + const rules = await getRules({ alertsClient, filter: '' }); expect(rules).toEqual([result1, result2]); }); }); @@ -209,11 +185,7 @@ describe('get_existing_prepackaged_rules', () => { test('it returns a count', async () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRulesCount({ - alertsClient: unsafeCast, - filter: '', - }); + const rules = await getRulesCount({ alertsClient, filter: '' }); expect(rules).toEqual(1); }); }); @@ -222,10 +194,7 @@ describe('get_existing_prepackaged_rules', () => { test('it returns a count', async () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRulesCount({ - alertsClient: unsafeCast, - }); + const rules = await getNonPackagedRulesCount({ alertsClient }); expect(rules).toEqual(1); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts index ff48b9f5f7c33..35d3489dad6fc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts @@ -10,7 +10,6 @@ import { getFindResultWithSingleHit, FindHit, } from '../routes/__mocks__/request_responses'; -import { AlertsClient } from '../../../../../alerting'; import { getExportAll } from './get_export_all'; describe('getExportAll', () => { @@ -19,8 +18,7 @@ describe('getExportAll', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const exports = await getExportAll(unsafeCast); + const exports = await getExportAll(alertsClient); expect(exports).toEqual({ rulesNdjson: '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"saved_id":"some-id","timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n', @@ -39,8 +37,7 @@ describe('getExportAll', () => { alertsClient.find.mockResolvedValue(findResult); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const exports = await getExportAll(unsafeCast); + const exports = await getExportAll(alertsClient); expect(exports).toEqual({ rulesNdjson: '', exportDetails: '{"exported_count":0,"missing_rules":[],"missing_rules_count":0}\n', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 236d04acc782b..4b6ea527a2027 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -11,7 +11,6 @@ import { getFindResultWithSingleHit, FindHit, } from '../routes/__mocks__/request_responses'; -import { AlertsClient } from '../../../../../alerting'; describe('get_export_by_object_ids', () => { describe('getExportByObjectIds', () => { @@ -20,9 +19,8 @@ describe('get_export_by_object_ids', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const objects = [{ rule_id: 'rule-1' }]; - const exports = await getExportByObjectIds(unsafeCast, objects); + const exports = await getExportByObjectIds(alertsClient, objects); expect(exports).toEqual({ rulesNdjson: '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"saved_id":"some-id","timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n', @@ -45,9 +43,8 @@ describe('get_export_by_object_ids', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(findResult); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const objects = [{ rule_id: 'rule-1' }]; - const exports = await getExportByObjectIds(unsafeCast, objects); + const exports = await getExportByObjectIds(alertsClient, objects); expect(exports).toEqual({ rulesNdjson: '', exportDetails: @@ -62,9 +59,8 @@ describe('get_export_by_object_ids', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(unsafeCast, objects); + const exports = await getRulesFromObjects(alertsClient, objects); const expected: RulesErrors = { exportedCount: 1, missingRules: [], @@ -138,9 +134,8 @@ describe('get_export_by_object_ids', () => { alertsClient.get.mockResolvedValue(result); alertsClient.find.mockResolvedValue(findResult); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(unsafeCast, objects); + const exports = await getRulesFromObjects(alertsClient, objects); const expected: RulesErrors = { exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], @@ -162,9 +157,8 @@ describe('get_export_by_object_ids', () => { alertsClient.get.mockRejectedValue({ output: { statusCode: 404 } }); alertsClient.find.mockResolvedValue(findResult); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(unsafeCast, objects); + const exports = await getRulesFromObjects(alertsClient, objects); const expected: RulesErrors = { exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts index 6ba0aa95bdd7b..c637860c5646a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts @@ -6,7 +6,6 @@ import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; import { readRules } from './read_rules'; -import { AlertsClient } from '../../../../../alerting'; import { getResult, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; describe('read_rules', () => { @@ -15,9 +14,8 @@ describe('read_rules', () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: undefined, }); @@ -28,9 +26,8 @@ describe('read_rules', () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: null, }); @@ -42,9 +39,8 @@ describe('read_rules', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: undefined, ruleId: 'rule-1', }); @@ -56,9 +52,8 @@ describe('read_rules', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: null, ruleId: 'rule-1', }); @@ -70,9 +65,8 @@ describe('read_rules', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: null, ruleId: null, }); @@ -84,9 +78,8 @@ describe('read_rules', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: undefined, ruleId: undefined, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index 8c44d82f46b53..8579447a74c69 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -14,10 +14,10 @@ import { SavedObjectsClientContract, } from 'kibana/server'; import { SIGNALS_ID } from '../../../../common/constants'; +import { LegacyRequest } from '../../../types'; import { AlertsClient } from '../../../../../alerting/server'; import { ActionsClient } from '../../../../../../../plugins/actions/server'; import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types'; -import { RequestFacade } from '../../../types'; import { Alert } from '../../../../../alerting/server/types'; export type PatchRuleAlertParamsRest = Partial & { @@ -39,19 +39,19 @@ export interface FindParamsRest { filter: string; } -export interface PatchRulesRequest extends RequestFacade { +export interface PatchRulesRequest extends LegacyRequest { payload: PatchRuleAlertParamsRest; } -export interface BulkPatchRulesRequest extends RequestFacade { +export interface BulkPatchRulesRequest extends LegacyRequest { payload: PatchRuleAlertParamsRest[]; } -export interface UpdateRulesRequest extends RequestFacade { +export interface UpdateRulesRequest extends LegacyRequest { payload: UpdateRuleAlertParamsRest; } -export interface BulkUpdateRulesRequest extends RequestFacade { +export interface BulkUpdateRulesRequest extends LegacyRequest { payload: UpdateRuleAlertParamsRest[]; } @@ -99,11 +99,11 @@ export interface IRuleStatusFindType { export type RuleStatusString = 'succeeded' | 'failed' | 'going to run' | 'executing'; -export interface RulesRequest extends RequestFacade { +export interface RulesRequest extends LegacyRequest { payload: RuleAlertParamsRest; } -export interface BulkRulesRequest extends RequestFacade { +export interface BulkRulesRequest extends LegacyRequest { payload: RuleAlertParamsRest[]; } @@ -112,12 +112,12 @@ export interface HapiReadableStream extends Readable { filename: string; }; } -export interface ImportRulesRequest extends Omit { +export interface ImportRulesRequest extends Omit { query: { overwrite: boolean }; payload: { file: HapiReadableStream }; } -export interface ExportRulesRequest extends Omit { +export interface ExportRulesRequest extends Omit { payload: { objects: Array<{ rule_id: string }> | null | undefined }; query: { file_name: string; @@ -125,11 +125,11 @@ export interface ExportRulesRequest extends Omit { }; } -export type QueryRequest = Omit & { +export type QueryRequest = Omit & { query: { id: string | undefined; rule_id: string | undefined }; }; -export interface QueryBulkRequest extends RequestFacade { +export interface QueryBulkRequest extends LegacyRequest { payload: Array; } @@ -143,7 +143,7 @@ export interface FindRuleParams { sortOrder?: 'asc' | 'desc'; } -export interface FindRulesRequest extends Omit { +export interface FindRulesRequest extends Omit { query: { per_page: number; page: number; @@ -155,7 +155,7 @@ export interface FindRulesRequest extends Omit { }; } -export interface FindRulesStatusesRequest extends Omit { +export interface FindRulesStatusesRequest extends Omit { query: { ids: string[]; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts index 9b7b2b8f1fff9..e9159ab87a0c0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -6,7 +6,7 @@ import { RuleAlertParams, OutputRuleAlertRest } from '../types'; import { SearchResponse } from '../../types'; -import { RequestFacade } from '../../../types'; +import { LegacyRequest } from '../../../types'; import { AlertType, State, AlertExecutorOptions } from '../../../../../alerting/server/types'; export interface SignalsParams { @@ -35,11 +35,11 @@ export type SignalsStatusRestParams = Omit & { export type SignalsQueryRestParams = SignalQueryParams; -export interface SignalsStatusRequest extends RequestFacade { +export interface SignalsStatusRequest extends LegacyRequest { payload: SignalsStatusRestParams; } -export interface SignalsQueryRequest extends RequestFacade { +export interface SignalsQueryRequest extends LegacyRequest { payload: SignalsQueryRestParams; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts index 87739bf785012..940011924de79 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts @@ -5,7 +5,6 @@ */ import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; -import { AlertsClient } from '../../../../../alerting'; import { getResult, getFindResultWithMultiHits } from '../routes/__mocks__/request_responses'; import { INTERNAL_RULE_ID_KEY, INTERNAL_IDENTIFIER } from '../../../../common/constants'; import { readRawTags, readTags, convertTagsToSet, convertToTags, isTags } from './read_tags'; @@ -30,10 +29,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); + const tags = await readRawTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -51,10 +47,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); + const tags = await readRawTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -72,10 +65,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); + const tags = await readRawTags({ alertsClient }); expect(tags).toEqual([]); }); @@ -88,10 +78,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); + const tags = await readRawTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2']); }); @@ -104,10 +91,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); + const tags = await readRawTags({ alertsClient }); expect(tags).toEqual([]); }); }); @@ -127,10 +111,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -148,10 +129,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -169,10 +147,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual([]); }); @@ -185,10 +160,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2']); }); @@ -201,10 +173,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual([]); }); @@ -221,10 +190,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual(['tag 1']); }); @@ -257,10 +223,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4', 'tag 5']); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index e15053db75777..08cb2e7bc19ee 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CallAPIOptions } from '../../../../../../../src/core/server'; import { Filter } from '../../../../../../../src/plugins/data/server'; import { IRuleStatusAttributes } from './rules/types'; @@ -117,4 +118,9 @@ export type PrepackagedRules = Omit< | 'created_at' > & { rule_id: string; immutable: boolean }; -export type CallWithRequest = (endpoint: string, params: T, options?: U) => Promise; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type CallWithRequest, V> = ( + endpoint: string, + params: T, + options?: CallAPIOptions +) => Promise; diff --git a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts index b1f0c3c4a3a18..42dc13d84fd98 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts @@ -519,7 +519,6 @@ describe('events elasticsearch_adapter', () => { return mockResponseMap; }); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/events/mock.ts b/x-pack/legacy/plugins/siem/server/lib/events/mock.ts index 195c0cd674af5..3eb841cbad411 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/mock.ts @@ -189,8 +189,7 @@ export const mockOptions: RequestDetailsOptions = { }; export const mockRequest = { - params: {}, - payload: { + body: { operationName: 'GetNetworkTopNFlowQuery', variables: { indexName: 'auditbeat-8.0.0-2019.03.29-000003', diff --git a/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts index 39f75e6ea36c3..4cce0b0999257 100644 --- a/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts @@ -9,35 +9,27 @@ import { GraphQLSchema } from 'graphql'; import { runHttpQuery } from 'apollo-server-core'; import { schema as configSchema } from '@kbn/config-schema'; import { - CoreSetup, IRouter, KibanaResponseFactory, RequestHandlerContext, - PluginInitializerContext, KibanaRequest, } from '../../../../../../../src/core/server'; import { IndexPatternsFetcher } from '../../../../../../../src/plugins/data/server'; import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; -import { RequestFacade } from '../../types'; +import { CoreSetup, SetupPlugins } from '../../plugin'; import { FrameworkAdapter, FrameworkIndexPatternsService, FrameworkRequest, internalFrameworkRequest, - WrappableRequest, } from './types'; -import { SiemPluginSecurity, PluginsSetup } from '../../plugin'; export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { - public version: string; - private isProductionMode: boolean; private router: IRouter; - private security: SiemPluginSecurity; + private security: SetupPlugins['security']; - constructor(core: CoreSetup, plugins: PluginsSetup, env: PluginInitializerContext['env']) { - this.version = env.packageInfo.version; - this.isProductionMode = env.mode.prod; + constructor(core: CoreSetup, plugins: SetupPlugins, private isProductionMode: boolean) { this.router = core.http.createRouter(); this.security = plugins.security; } @@ -68,13 +60,7 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { this.router.post( { path: routePath, - validate: { - body: configSchema.object({ - operationName: configSchema.string(), - query: configSchema.string(), - variables: configSchema.object({}, { allowUnknowns: true }), - }), - }, + validate: { body: configSchema.object({}, { allowUnknowns: true }) }, options: { tags: ['access:siem'], }, @@ -84,7 +70,7 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { const user = await this.getCurrentUserInfo(request); const gqlResponse = await runHttpQuery([request], { method: 'POST', - options: (req: RequestFacade) => ({ + options: (req: KibanaRequest) => ({ context: { req: wrapRequest(req, context, user) }, schema, }), @@ -104,39 +90,6 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { ); if (!this.isProductionMode) { - this.router.get( - { - path: routePath, - validate: { query: configSchema.object({}, { allowUnknowns: true }) }, - options: { - tags: ['access:siem'], - }, - }, - async (context, request, response) => { - try { - const user = await this.getCurrentUserInfo(request); - const { query } = request; - const gqlResponse = await runHttpQuery([request], { - method: 'GET', - options: (req: RequestFacade) => ({ - context: { req: wrapRequest(req, context, user) }, - schema, - }), - query, - }); - - return response.ok({ - body: gqlResponse, - headers: { - 'content-type': 'application/json', - }, - }); - } catch (error) { - return this.handleError(error, response); - } - } - ); - this.router.get( { path: `${routePath}/graphiql`, @@ -150,7 +103,7 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { request.query, { endpointURL: routePath, - passHeader: `'kbn-version': '${this.version}'`, + passHeader: "'kbn-xsrf': 'graphiql'", }, request ); @@ -208,20 +161,15 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { } } -export function wrapRequest( - req: InternalRequest, +export function wrapRequest( + request: KibanaRequest, context: RequestHandlerContext, user: AuthenticatedUser | null -): FrameworkRequest { - const { auth, params, payload, query } = req; - +): FrameworkRequest { return { - [internalFrameworkRequest]: req, - auth, + [internalFrameworkRequest]: request, + body: request.body, context, - params, - payload, - query, user, }; } 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 67861ce0dcf28..9fc78e6fb84fe 100644 --- a/x-pack/legacy/plugins/siem/server/lib/framework/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/framework/types.ts @@ -6,9 +6,8 @@ import { IndicesGetMappingParams } from 'elasticsearch'; import { GraphQLSchema } from 'graphql'; -import { RequestAuth } from 'hapi'; -import { RequestHandlerContext } from '../../../../../../../src/core/server'; +import { RequestHandlerContext, KibanaRequest } from '../../../../../../../src/core/server'; import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; import { ESQuery } from '../../../common/typed_json'; import { @@ -19,14 +18,12 @@ import { TimerangeInput, Maybe, } from '../../graphql/types'; -import { RequestFacade } from '../../types'; export * from '../../utils/typed_resolvers'; export const internalFrameworkRequest = Symbol('internalFrameworkRequest'); export interface FrameworkAdapter { - version: string; registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void; callWithRequest( req: FrameworkRequest, @@ -46,24 +43,12 @@ export interface FrameworkAdapter { getIndexPatternsService(req: FrameworkRequest): FrameworkIndexPatternsService; } -export interface FrameworkRequest { - [internalFrameworkRequest]: InternalRequest; +export interface FrameworkRequest extends Pick { + [internalFrameworkRequest]: KibanaRequest; context: RequestHandlerContext; - payload: InternalRequest['payload']; - params: InternalRequest['params']; - query: InternalRequest['query']; - auth: InternalRequest['auth']; user: AuthenticatedUser | null; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface WrappableRequest { - payload: Payload; - params: Params; - query: Query; - auth: RequestAuth; -} - export interface DatabaseResponse { took: number; timeout: boolean; diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts index 0d698f1e19213..20510e1089f96 100644 --- a/x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts @@ -159,7 +159,6 @@ describe('hosts elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockGetHostsResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -180,7 +179,6 @@ describe('hosts elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockGetHostOverviewResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -201,7 +199,6 @@ describe('hosts elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockGetHostLastFirstSeenResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/mock.ts b/x-pack/legacy/plugins/siem/server/lib/hosts/mock.ts index 66b73742cc45e..6b72c4a5a2843 100644 --- a/x-pack/legacy/plugins/siem/server/lib/hosts/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/hosts/mock.ts @@ -49,8 +49,7 @@ export const mockGetHostsOptions: HostsRequestOptions = { }; export const mockGetHostsRequest = { - params: {}, - payload: { + body: { operationName: 'GetHostsTableQuery', variables: { sourceId: 'default', @@ -67,7 +66,6 @@ export const mockGetHostsRequest = { query: 'query GetHostsTableQuery($sourceId: ID!, $timerange: TimerangeInput!, $pagination: PaginationInput!, $sort: HostsSortField!, $filterQuery: String) {\n source(id: $sourceId) {\n id\n Hosts(timerange: $timerange, pagination: $pagination, sort: $sort, filterQuery: $filterQuery) {\n totalCount\n edges {\n node {\n _id\n host {\n id\n name\n os {\n name\n version\n __typename\n }\n __typename\n }\n __typename\n }\n cursor {\n value\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor {\n value\n __typename\n }\n hasNextPage\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockGetHostsResponse = { @@ -327,14 +325,12 @@ export const mockGetHostOverviewOptions: HostOverviewRequestOptions = { }; export const mockGetHostOverviewRequest = { - params: {}, - payload: { + body: { operationName: 'GetHostOverviewQuery', variables: { sourceId: 'default', hostName: 'siem-es' }, query: 'query GetHostOverviewQuery($sourceId: ID!, $hostName: String!, $timerange: TimerangeInput!) {\n source(id: $sourceId) {\n id\n HostOverview(hostName: $hostName, timerange: $timerange) {\n _id\n host {\n architecture\n id\n ip\n mac\n name\n os {\n family\n name\n platform\n version\n __typename\n }\n type\n __typename\n }\n cloud {\n instance {\n id\n __typename\n }\n machine {\n type\n __typename\n }\n provider\n region\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockGetHostOverviewResponse = { @@ -520,14 +516,12 @@ export const mockGetHostLastFirstSeenOptions: HostLastFirstSeenRequestOptions = }; export const mockGetHostLastFirstSeenRequest = { - params: {}, - payload: { + body: { operationName: 'GetHostLastFirstSeenQuery', variables: { sourceId: 'default', hostName: 'siem-es' }, query: 'query GetHostLastFirstSeenQuery($sourceId: ID!, $hostName: String!) {\n source(id: $sourceId) {\n id\n HostLastFirstSeen(hostName: $hostName) {\n firstSeen\n lastSeen\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockGetHostLastFirstSeenResponse = { diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts index 4a179073852b0..059d15220b619 100644 --- a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts @@ -53,7 +53,6 @@ describe('getKpiHosts', () => { let data: KpiHostsData; const mockCallWithRequest = jest.fn(); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -167,7 +166,6 @@ describe('getKpiHostDetails', () => { let data: KpiHostDetailsData; const mockCallWithRequest = jest.fn(); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/mock.ts b/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/mock.ts index a1962067f9bec..b82a540900bd0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/mock.ts @@ -43,8 +43,7 @@ export const mockKpiHostDetailsOptions: RequestBasicOptions = { }; export const mockKpiHostsRequest = { - params: {}, - payload: { + body: { operationName: 'GetKpiHostsQuery', variables: { sourceId: 'default', @@ -54,12 +53,10 @@ export const mockKpiHostsRequest = { query: 'fragment KpiHostChartFields on KpiHostHistogramData {\n x\n y\n __typename\n}\n\nquery GetKpiHostsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String, $defaultIndex: [String!]!) {\n source(id: $sourceId) {\n id\n KpiHosts(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex) {\n hosts\n hostsHistogram {\n ...KpiHostChartFields\n __typename\n }\n authSuccess\n authSuccessHistogram {\n ...KpiHostChartFields\n __typename\n }\n authFailure\n authFailureHistogram {\n ...KpiHostChartFields\n __typename\n }\n uniqueSourceIps\n uniqueSourceIpsHistogram {\n ...KpiHostChartFields\n __typename\n }\n uniqueDestinationIps\n uniqueDestinationIpsHistogram {\n ...KpiHostChartFields\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockKpiHostDetailsRequest = { - params: {}, - payload: { + body: { operationName: 'GetKpiHostDetailsQuery', variables: { sourceId: 'default', @@ -69,7 +66,6 @@ export const mockKpiHostDetailsRequest = { query: 'fragment KpiHostDetailsChartFields on KpiHostHistogramData {\n x\n y\n __typename\n}\n\nquery GetKpiHostDetailsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String, $defaultIndex: [String!]!, $hostName: String!) {\n source(id: $sourceId) {\n id\n KpiHostDetails(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex, hostName: $hostName) {\n authSuccess\n authSuccessHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n authFailure\n authFailureHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n uniqueSourceIps\n uniqueSourceIpsHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n uniqueDestinationIps\n uniqueDestinationIpsHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; const mockUniqueIpsResponse = { diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts index 11d007f591fac..58ee7c9aa1cf8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts @@ -48,7 +48,6 @@ describe('Network Kpi elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/mock.ts b/x-pack/legacy/plugins/siem/server/lib/kpi_network/mock.ts index 5b0601b88c779..7d86769de09f1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/kpi_network/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/kpi_network/mock.ts @@ -24,8 +24,7 @@ export const mockOptions: RequestBasicOptions = { }; export const mockRequest = { - params: {}, - payload: { + body: { operationName: 'GetKpiNetworkQuery', variables: { sourceId: 'default', @@ -35,7 +34,6 @@ export const mockRequest = { query: 'fragment KpiNetworkChartFields on KpiNetworkHistogramData {\n x\n y\n __typename\n}\n\nquery GetKpiNetworkQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String, $defaultIndex: [String!]!) {\n source(id: $sourceId) {\n id\n KpiNetwork(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex) {\n networkEvents\n uniqueFlowId\n uniqueSourcePrivateIps\n uniqueSourcePrivateIpsHistogram {\n ...KpiNetworkChartFields\n __typename\n }\n uniqueDestinationPrivateIps\n uniqueDestinationPrivateIpsHistogram {\n ...KpiNetworkChartFields\n __typename\n }\n dnsQueries\n tlsHandshakes\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockResponse = { diff --git a/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts index eeea4bec2fb25..eab461ee07ca7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts @@ -35,7 +35,6 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, getIndexPatternsService: jest.fn(), registerGraphQLEndpoint: jest.fn(), @@ -61,7 +60,6 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockNoDataResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -101,7 +99,6 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = ].buckets[0].location.top_geo.hits.hits = []; mockCallWithRequest.mockResolvedValue(mockNoGeoDataResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, getIndexPatternsService: jest.fn(), registerGraphQLEndpoint: jest.fn(), @@ -132,7 +129,6 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockNoPaginationResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -155,7 +151,6 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockResponseIp); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, getIndexPatternsService: jest.fn(), registerGraphQLEndpoint: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/network/mock.ts b/x-pack/legacy/plugins/siem/server/lib/network/mock.ts index 21b00bf188d20..7ea692f27ef04 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/mock.ts @@ -59,8 +59,7 @@ export const mockOptions: NetworkTopNFlowRequestOptions = { }; export const mockRequest = { - params: {}, - payload: { + body: { operationName: 'GetNetworkTopNFlowQuery', variables: { filterQuery: '', @@ -1507,10 +1506,10 @@ export const mockOptionsIp: NetworkTopNFlowRequestOptions = { export const mockRequestIp = { ...mockRequest, - payload: { - ...mockRequest.payload, + body: { + ...mockRequest.body, variables: { - ...mockRequest.payload.variables, + ...mockRequest.body.variables, ip: '1.1.1.1', }, }, diff --git a/x-pack/legacy/plugins/siem/server/lib/overview/elastic_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/overview/elastic_adapter.test.ts index 29035f4539be8..f421704dffe12 100644 --- a/x-pack/legacy/plugins/siem/server/lib/overview/elastic_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/overview/elastic_adapter.test.ts @@ -36,7 +36,6 @@ describe('Siem Overview elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockResponseNetwork); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -70,7 +69,6 @@ describe('Siem Overview elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockNoDataResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -108,7 +106,6 @@ describe('Siem Overview elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockResponseHost); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -148,7 +145,6 @@ describe('Siem Overview elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockNoDataResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/overview/mock.ts b/x-pack/legacy/plugins/siem/server/lib/overview/mock.ts index 6196f45029313..410b4d90b1e78 100644 --- a/x-pack/legacy/plugins/siem/server/lib/overview/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/overview/mock.ts @@ -24,8 +24,7 @@ export const mockOptionsNetwork: RequestBasicOptions = { }; export const mockRequestNetwork = { - params: {}, - payload: { + body: { operationName: 'GetOverviewNetworkQuery', variables: { sourceId: 'default', @@ -35,7 +34,6 @@ export const mockRequestNetwork = { query: 'query GetOverviewNetworkQuery(\n $sourceId: ID!\n $timerange: TimerangeInput!\n $filterQuery: String\n ) {\n source(id: $sourceId) {\n id\n OverviewNetwork(timerange: $timerange, filterQuery: $filterQuery) {\n packetbeatFlow\n packetbeatDNS\n filebeatSuricata\n filebeatZeek\n auditbeatSocket\n }\n }\n }', }, - query: {}, }; export const mockResponseNetwork = { @@ -97,8 +95,7 @@ export const mockOptionsHost: RequestBasicOptions = { }; export const mockRequestHost = { - params: {}, - payload: { + body: { operationName: 'GetOverviewHostQuery', variables: { sourceId: 'default', @@ -108,7 +105,6 @@ export const mockRequestHost = { query: 'query GetOverviewHostQuery(\n $sourceId: ID!\n $timerange: TimerangeInput!\n $filterQuery: String\n ) {\n source(id: $sourceId) {\n id\n OverviewHost(timerange: $timerange, filterQuery: $filterQuery) {\n auditbeatAuditd\n auditbeatFIM\n auditbeatLogin\n auditbeatPackage\n auditbeatProcess\n auditbeatUser\n }\n }\n }', }, - query: {}, }; export const mockResponseHost = { diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts index 32a5c72215dda..428685cbaddb8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts @@ -22,7 +22,6 @@ describe('elasticsearch_adapter', () => { let data: TlsData; const mockCallWithRequest = jest.fn(); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts b/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts index a81862b6e7e90..4b27d541ec992 100644 --- a/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts @@ -212,8 +212,7 @@ export const expectedTlsEdges = [ ]; export const mockRequest = { - params: {}, - payload: { + body: { operationName: 'GetTlsQuery', variables: { defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -229,7 +228,6 @@ export const mockRequest = { query: 'query GetTlsQuery($sourceId: ID!, $filterQuery: String, $flowTarget: FlowTarget!, $ip: String!, $pagination: PaginationInputPaginated!, $sort: TlsSortField!, $timerange: TimerangeInput!, $defaultIndex: [String!]!, $inspect: Boolean!) {\n source(id: $sourceId) {\n id\n Tls(filterQuery: $filterQuery, flowTarget: $flowTarget, ip: $ip, pagination: $pagination, sort: $sort, timerange: $timerange, defaultIndex: $defaultIndex) {\n totalCount\n edges {\n node {\n _id\n alternativeNames\n commonNames\n ja3\n issuerNames\n notAfter\n __typename\n }\n cursor {\n value\n __typename\n }\n __typename\n }\n pageInfo {\n activePage\n fakeTotalCount\n showMorePagesIndicator\n __typename\n }\n inspect @include(if: $inspect) {\n dsl\n response\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockResponse = { diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index 9034ab4e6af83..34a50cf962412 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +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'; @@ -54,6 +57,8 @@ export interface AppBackendLibs extends AppDomainLibs { export interface SiemContext { req: FrameworkRequest; + context: RequestHandlerContext; + user: AuthenticatedUser | null; } export interface TotalValue { diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts index 94314367be59c..e15248e5200ee 100644 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ b/x-pack/legacy/plugins/siem/server/plugin.ts @@ -5,39 +5,71 @@ */ import { i18n } from '@kbn/i18n'; -import { CoreSetup, PluginInitializerContext, Logger } from 'src/core/server'; -import { SecurityPluginSetup } from '../../../../plugins/security/server'; -import { PluginSetupContract as FeaturesSetupContract } from '../../../../plugins/features/server'; + +import { + CoreSetup, + CoreStart, + PluginInitializerContext, + Logger, +} from '../../../../../src/core/server'; +import { SecurityPluginSetup as SecuritySetup } from '../../../../plugins/security/server'; +import { PluginSetupContract as FeaturesSetup } from '../../../../plugins/features/server'; +import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from '../../../../plugins/encrypted_saved_objects/server'; +import { SpacesPluginSetup as SpacesSetup } from '../../../../plugins/spaces/server'; +import { PluginStartContract as ActionsStart } from '../../../../plugins/actions/server'; +import { LegacyServices } from './types'; import { initServer } from './init_server'; import { compose } from './lib/compose/kibana'; +import { initRoutes, LegacyInitRoutes } from './routes'; +import { isAlertExecutor } from './lib/detection_engine/signals/types'; +import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type'; import { noteSavedObjectType, pinnedEventSavedObjectType, timelineSavedObjectType, ruleStatusSavedObjectType, } from './saved_objects'; +import { ClientsService } from './services'; + +export { CoreSetup, CoreStart }; -export type SiemPluginSecurity = Pick; +export interface SetupPlugins { + encryptedSavedObjects: EncryptedSavedObjectsSetup; + features: FeaturesSetup; + security: SecuritySetup; + spaces?: SpacesSetup; +} -export interface PluginsSetup { - features: FeaturesSetupContract; - security: SiemPluginSecurity; +export interface StartPlugins { + actions: ActionsStart; } export class Plugin { readonly name = 'siem'; private readonly logger: Logger; private context: PluginInitializerContext; + private clients: ClientsService; + private legacyInitRoutes?: LegacyInitRoutes; constructor(context: PluginInitializerContext) { this.context = context; this.logger = context.logger.get('plugins', this.name); + this.clients = new ClientsService(); this.logger.debug('Shim plugin initialized'); } - public setup(core: CoreSetup, plugins: PluginsSetup) { + public setup(core: CoreSetup, plugins: SetupPlugins, __legacy: LegacyServices) { this.logger.debug('Shim plugin setup'); + + this.clients.setup(core.elasticsearch.dataClient, plugins.spaces?.spacesService); + + this.legacyInitRoutes = initRoutes( + __legacy.route, + __legacy.config, + plugins.encryptedSavedObjects?.usingEphemeralEncryptionKey ?? false + ); + plugins.features.registerFeature({ id: this.name, name: i18n.translate('xpack.siem.featureRegistry.linkSiemTitle', { @@ -98,7 +130,23 @@ export class Plugin { }, }); - const libs = compose(core, plugins, this.context.env); + if (__legacy.alerting != null) { + const type = signalRulesAlertType({ + logger: this.logger, + version: this.context.env.packageInfo.version, + }); + if (isAlertExecutor(type)) { + __legacy.alerting.setup.registerType(type); + } + } + + const libs = compose(core, plugins, this.context.env.mode.prod); initServer(libs); } + + public start(core: CoreStart, plugins: StartPlugins) { + this.clients.start(core.savedObjects, plugins.actions); + + this.legacyInitRoutes!(this.clients.createGetScoped()); + } } diff --git a/x-pack/legacy/plugins/siem/server/routes/index.ts b/x-pack/legacy/plugins/siem/server/routes/index.ts new file mode 100644 index 0000000000000..82fc4d8c11722 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/routes/index.ts @@ -0,0 +1,78 @@ +/* + * 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 { LegacyServices } from '../types'; +import { GetScopedClients } from '../services'; + +import { createRulesRoute } from '../lib/detection_engine/routes/rules/create_rules_route'; +import { createIndexRoute } from '../lib/detection_engine/routes/index/create_index_route'; +import { readIndexRoute } from '../lib/detection_engine/routes/index/read_index_route'; +import { readRulesRoute } from '../lib/detection_engine/routes/rules/read_rules_route'; +import { findRulesRoute } from '../lib/detection_engine/routes/rules/find_rules_route'; +import { deleteRulesRoute } from '../lib/detection_engine/routes/rules/delete_rules_route'; +import { updateRulesRoute } from '../lib/detection_engine/routes/rules/update_rules_route'; +import { patchRulesRoute } from '../lib/detection_engine/routes/rules/patch_rules_route'; +import { setSignalsStatusRoute } from '../lib/detection_engine/routes/signals/open_close_signals_route'; +import { querySignalsRoute } from '../lib/detection_engine/routes/signals/query_signals_route'; +import { deleteIndexRoute } from '../lib/detection_engine/routes/index/delete_index_route'; +import { readTagsRoute } from '../lib/detection_engine/routes/tags/read_tags_route'; +import { readPrivilegesRoute } from '../lib/detection_engine/routes/privileges/read_privileges_route'; +import { addPrepackedRulesRoute } from '../lib/detection_engine/routes/rules/add_prepackaged_rules_route'; +import { createRulesBulkRoute } from '../lib/detection_engine/routes/rules/create_rules_bulk_route'; +import { updateRulesBulkRoute } from '../lib/detection_engine/routes/rules/update_rules_bulk_route'; +import { patchRulesBulkRoute } from '../lib/detection_engine/routes/rules/patch_rules_bulk_route'; +import { deleteRulesBulkRoute } from '../lib/detection_engine/routes/rules/delete_rules_bulk_route'; +import { importRulesRoute } from '../lib/detection_engine/routes/rules/import_rules_route'; +import { exportRulesRoute } from '../lib/detection_engine/routes/rules/export_rules_route'; +import { findRulesStatusesRoute } from '../lib/detection_engine/routes/rules/find_rules_status_route'; +import { getPrepackagedRulesStatusRoute } from '../lib/detection_engine/routes/rules/get_prepackaged_rules_status_route'; + +export type LegacyInitRoutes = (getClients: GetScopedClients) => void; + +export const initRoutes = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + usingEphemeralEncryptionKey: boolean +) => (getClients: GetScopedClients): void => { + // Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules + // All REST rule creation, deletion, updating, etc...... + createRulesRoute(route, config, getClients); + readRulesRoute(route, getClients); + updateRulesRoute(route, config, getClients); + patchRulesRoute(route, getClients); + deleteRulesRoute(route, getClients); + findRulesRoute(route, getClients); + + addPrepackedRulesRoute(route, config, getClients); + getPrepackagedRulesStatusRoute(route, getClients); + createRulesBulkRoute(route, config, getClients); + updateRulesBulkRoute(route, config, getClients); + patchRulesBulkRoute(route, getClients); + deleteRulesBulkRoute(route, getClients); + + importRulesRoute(route, config, getClients); + exportRulesRoute(route, config, getClients); + + findRulesStatusesRoute(route, getClients); + + // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals + // POST /api/detection_engine/signals/status + // Example usage can be found in siem/server/lib/detection_engine/scripts/signals + setSignalsStatusRoute(route, config, getClients); + querySignalsRoute(route, config, getClients); + + // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index + // All REST index creation, policy management for spaces + createIndexRoute(route, config, getClients); + readIndexRoute(route, config, getClients); + deleteIndexRoute(route, config, getClients); + + // Detection Engine tags routes that have the REST endpoints of /api/detection_engine/tags + readTagsRoute(route, getClients); + + // Privileges API to get the generic user privileges + readPrivilegesRoute(route, config, usingEphemeralEncryptionKey, getClients); +}; diff --git a/x-pack/legacy/plugins/siem/server/services/clients.test.ts b/x-pack/legacy/plugins/siem/server/services/clients.test.ts new file mode 100644 index 0000000000000..7f63a8f5e949c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/services/clients.test.ts @@ -0,0 +1,32 @@ +/* + * 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 { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { actionsMock } from '../../../../../plugins/actions/server/mocks'; + +import { ClientsService } from './clients'; + +describe('ClientsService', () => { + describe('spacesClient', () => { + describe('#getSpaceId', () => { + it('returns the default spaceId if spaces are disabled', async () => { + const clients = new ClientsService(); + + const actions = actionsMock.createStart(); + const { elasticsearch } = coreMock.createSetup(); + const { savedObjects } = coreMock.createStart(); + const request = httpServerMock.createRawRequest(); + const spacesService = undefined; + + clients.setup(elasticsearch.dataClient, spacesService); + clients.start(savedObjects, actions); + + const { spacesClient } = await clients.createGetScoped()(request); + expect(spacesClient.getSpaceId()).toEqual('default'); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/services/clients.ts b/x-pack/legacy/plugins/siem/server/services/clients.ts new file mode 100644 index 0000000000000..ca50eda4e7a6c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/services/clients.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IClusterClient, + IScopedClusterClient, + KibanaRequest, + LegacyRequest, + SavedObjectsClientContract, +} from '../../../../../../src/core/server'; +import { ActionsClient } from '../../../../../plugins/actions/server'; +import { AlertsClient } from '../../../../../legacy/plugins/alerting/server'; +import { SpacesServiceSetup } from '../../../../../plugins/spaces/server'; +import { CoreStart, StartPlugins } from '../plugin'; + +export interface Clients { + actionsClient?: ActionsClient; + clusterClient: IScopedClusterClient; + spacesClient: { getSpaceId: () => string }; + savedObjectsClient: SavedObjectsClientContract; +} +interface LegacyClients { + alertsClient?: AlertsClient; +} +export type GetScopedClients = (request: LegacyRequest) => Promise; + +export class ClientsService { + private actions?: StartPlugins['actions']; + private clusterClient?: IClusterClient; + private savedObjects?: CoreStart['savedObjects']; + private spacesService?: SpacesServiceSetup; + + public setup(clusterClient: IClusterClient, spacesService?: SpacesServiceSetup) { + this.clusterClient = clusterClient; + this.spacesService = spacesService; + } + + public start(savedObjects: CoreStart['savedObjects'], actions: StartPlugins['actions']) { + this.savedObjects = savedObjects; + this.actions = actions; + } + + public createGetScoped(): GetScopedClients { + if (!this.clusterClient || !this.savedObjects) { + throw new Error('Services not initialized'); + } + + return async (request: LegacyRequest) => { + const kibanaRequest = KibanaRequest.from(request); + + return { + alertsClient: request.getAlertsClient?.(), + actionsClient: await this.actions?.getActionsClientWithRequest?.(kibanaRequest), + clusterClient: this.clusterClient!.asScoped(kibanaRequest), + savedObjectsClient: this.savedObjects!.getScopedClient(kibanaRequest), + spacesClient: { + getSpaceId: () => this.spacesService?.getSpaceId?.(kibanaRequest) ?? 'default', + }, + }; + }; + } +} diff --git a/x-pack/legacy/plugins/siem/server/services/index.ts b/x-pack/legacy/plugins/siem/server/services/index.ts new file mode 100644 index 0000000000000..f4deea2c2a3fd --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/services/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ClientsService, GetScopedClients } from './clients'; diff --git a/x-pack/legacy/plugins/siem/server/types.ts b/x-pack/legacy/plugins/siem/server/types.ts index 7c07e63404eaa..e7831bb5d0451 100644 --- a/x-pack/legacy/plugins/siem/server/types.ts +++ b/x-pack/legacy/plugins/siem/server/types.ts @@ -6,28 +6,10 @@ import { Legacy } from 'kibana'; -export interface ServerFacade { +export { LegacyRequest } from '../../../../../src/core/server'; + +export interface LegacyServices { + alerting?: Legacy.Server['plugins']['alerting']; config: Legacy.Server['config']; - usingEphemeralEncryptionKey: boolean; - plugins: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - actions: any; // We have to do this at the moment because the types are not compatible - alerting?: Legacy.Server['plugins']['alerting']; - elasticsearch: Legacy.Server['plugins']['elasticsearch']; - spaces: Legacy.Server['plugins']['spaces']; - savedObjects: Legacy.Server['savedObjects']['SavedObjectsClient']; - }; route: Legacy.Server['route']; } - -export interface RequestFacade { - auth: Legacy.Request['auth']; - getAlertsClient?: Legacy.Request['getAlertsClient']; - getActionsClient?: Legacy.Request['getActionsClient']; - getSavedObjectsClient?: Legacy.Request['getSavedObjectsClient']; - headers: Legacy.Request['headers']; - method: Legacy.Request['method']; - params: Legacy.Request['params']; - payload: unknown; - query: Legacy.Request['query']; -} diff --git a/x-pack/plugins/siem/server/config.ts b/x-pack/plugins/siem/server/config.ts index 456646cc825f3..224043c0c6fe5 100644 --- a/x-pack/plugins/siem/server/config.ts +++ b/x-pack/plugins/siem/server/config.ts @@ -6,7 +6,7 @@ import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext } from '../../../../src/core/server'; import { SIGNALS_INDEX_KEY, DEFAULT_SIGNALS_INDEX, diff --git a/x-pack/plugins/siem/server/index.ts b/x-pack/plugins/siem/server/index.ts index c675be691b47e..83e2f900a3b90 100644 --- a/x-pack/plugins/siem/server/index.ts +++ b/x-pack/plugins/siem/server/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext } from '../../../../src/core/server'; import { Plugin } from './plugin'; import { configSchema, ConfigType } from './config'; diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index 866f4d7575e2f..ccc6aef1452b2 100644 --- a/x-pack/plugins/siem/server/plugin.ts +++ b/x-pack/plugins/siem/server/plugin.ts @@ -6,7 +6,7 @@ import { Observable } from 'rxjs'; -import { CoreSetup, PluginInitializerContext, Logger } from 'src/core/server'; +import { CoreSetup, PluginInitializerContext, Logger } from '../../../../src/core/server'; import { createConfig$, ConfigType } from './config'; export class Plugin { From fe3864282a0e3c26affc1bf3f34e235833f58041 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 14 Feb 2020 00:46:55 -0700 Subject: [PATCH 04/27] disable firefox smoke tests so we can fix flakiness out of band --- Jenkinsfile | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 4e6f3141a12e7..1b4350d5b91e9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -14,11 +14,11 @@ stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a 'kibana-intake-agent': kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh'), 'x-pack-intake-agent': kibanaPipeline.intakeWorker('x-pack-intake', './test/scripts/jenkins_xpack.sh'), 'kibana-oss-agent': kibanaPipeline.withWorkers('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - 'oss-firefoxSmoke': kibanaPipeline.getPostBuildWorker('firefoxSmoke', { - retryable('kibana-firefoxSmoke') { - runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') - } - }), + // 'oss-firefoxSmoke': kibanaPipeline.getPostBuildWorker('firefoxSmoke', { + // retryable('kibana-firefoxSmoke') { + // runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') + // } + // }), 'oss-ciGroup1': kibanaPipeline.getOssCiGroupWorker(1), 'oss-ciGroup2': kibanaPipeline.getOssCiGroupWorker(2), 'oss-ciGroup3': kibanaPipeline.getOssCiGroupWorker(3), @@ -39,11 +39,11 @@ stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a // 'oss-visualRegression': kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }), ]), 'kibana-xpack-agent': kibanaPipeline.withWorkers('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - 'xpack-firefoxSmoke': kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { - retryable('xpack-firefoxSmoke') { - runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') - } - }), + // 'xpack-firefoxSmoke': kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { + // retryable('xpack-firefoxSmoke') { + // runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') + // } + // }), 'xpack-ciGroup1': kibanaPipeline.getXpackCiGroupWorker(1), 'xpack-ciGroup2': kibanaPipeline.getXpackCiGroupWorker(2), 'xpack-ciGroup3': kibanaPipeline.getXpackCiGroupWorker(3), From 302c598e565e086b597692e5b4636b3516884030 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 14 Feb 2020 08:58:20 +0100 Subject: [PATCH 05/27] Preserve the original error name instead of returning raw AbortError (#57550) * Preserve the original error name instead of returning raw AbortError * use Error as the default error name --- .../kibana-plugin-public.ihttpfetcherror.md | 1 + src/core/public/http/fetch.test.ts | 42 ++++++++++++++++--- src/core/public/http/fetch.ts | 10 ++--- src/core/public/http/http_fetch_error.ts | 3 ++ src/core/public/http/types.ts | 1 + src/core/public/public.api.md | 2 + 6 files changed, 47 insertions(+), 12 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-public.ihttpfetcherror.md b/docs/development/core/public/kibana-plugin-public.ihttpfetcherror.md index 6109671bb1aa6..49287cc6e261e 100644 --- a/docs/development/core/public/kibana-plugin-public.ihttpfetcherror.md +++ b/docs/development/core/public/kibana-plugin-public.ihttpfetcherror.md @@ -16,6 +16,7 @@ export interface IHttpFetchError extends Error | Property | Type | Description | | --- | --- | --- | | [body](./kibana-plugin-public.ihttpfetcherror.body.md) | any | | +| [name](./kibana-plugin-public.ihttpfetcherror.name.md) | string | | | [req](./kibana-plugin-public.ihttpfetcherror.req.md) | Request | | | [request](./kibana-plugin-public.ihttpfetcherror.request.md) | Request | | | [res](./kibana-plugin-public.ihttpfetcherror.res.md) | Response | | diff --git a/src/core/public/http/fetch.test.ts b/src/core/public/http/fetch.test.ts index a99b7607d7149..efd9fdd053674 100644 --- a/src/core/public/http/fetch.test.ts +++ b/src/core/public/http/fetch.test.ts @@ -37,6 +37,7 @@ describe('Fetch', () => { }); afterEach(() => { fetchMock.restore(); + fetchInstance.removeAllInterceptors(); }); describe('http requests', () => { @@ -287,6 +288,42 @@ describe('Fetch', () => { }); }); + it('preserves the name of the original error', async () => { + expect.assertions(1); + + const abortError = new DOMException('The operation was aborted.', 'AbortError'); + + fetchMock.get('*', Promise.reject(abortError)); + + await fetchInstance.fetch('/my/path').catch(e => { + expect(e.name).toEqual('AbortError'); + }); + }); + + it('exposes the request to the interceptors in case of aborted request', async () => { + const responseErrorSpy = jest.fn(); + const abortError = new DOMException('The operation was aborted.', 'AbortError'); + + fetchMock.get('*', Promise.reject(abortError)); + + fetchInstance.intercept({ + responseError: responseErrorSpy, + }); + + await expect(fetchInstance.fetch('/my/path')).rejects.toThrow(); + + expect(responseErrorSpy).toHaveBeenCalledTimes(1); + const interceptedResponse = responseErrorSpy.mock.calls[0][0]; + + expect(interceptedResponse.request).toEqual( + expect.objectContaining({ + method: 'GET', + url: 'http://localhost/myBase/my/path', + }) + ); + expect(interceptedResponse.error.name).toEqual('AbortError'); + }); + it('should support get() helper', async () => { fetchMock.get('*', {}); await fetchInstance.get('/my/path', { method: 'POST' }); @@ -368,11 +405,6 @@ describe('Fetch', () => { fetchMock.get('*', { foo: 'bar' }); }); - afterEach(() => { - fetchMock.restore(); - fetchInstance.removeAllInterceptors(); - }); - it('should make request and receive response', async () => { fetchInstance.intercept({}); diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index 1043b50dff958..b433acdb6dbb9 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -146,11 +146,7 @@ export class Fetch { try { response = await window.fetch(request); } catch (err) { - if (err.name === 'AbortError') { - throw err; - } else { - throw new HttpFetchError(err.message, request); - } + throw new HttpFetchError(err.message, err.name ?? 'Error', request); } const contentType = response.headers.get('Content-Type') || ''; @@ -170,11 +166,11 @@ export class Fetch { } } } catch (err) { - throw new HttpFetchError(err.message, request, response, body); + throw new HttpFetchError(err.message, err.name ?? 'Error', request, response, body); } if (!response.ok) { - throw new HttpFetchError(response.statusText, request, response, body); + throw new HttpFetchError(response.statusText, 'Error', request, response, body); } return { fetchOptions, request, response, body }; diff --git a/src/core/public/http/http_fetch_error.ts b/src/core/public/http/http_fetch_error.ts index 2156df5798974..74aed4049613e 100644 --- a/src/core/public/http/http_fetch_error.ts +++ b/src/core/public/http/http_fetch_error.ts @@ -21,16 +21,19 @@ import { IHttpFetchError } from './types'; /** @internal */ export class HttpFetchError extends Error implements IHttpFetchError { + public readonly name: string; public readonly req: Request; public readonly res?: Response; constructor( message: string, + name: string, public readonly request: Request, public readonly response?: Response, public readonly body?: any ) { super(message); + this.name = name; this.req = request; this.res = response; diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index c38b9da442943..5909572c7e545 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -291,6 +291,7 @@ export interface IHttpResponseInterceptorOverrides { /** @public */ export interface IHttpFetchError extends Error { + readonly name: string; readonly request: Request; readonly response?: Response; /** diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 5e9b609bde916..f0289cc2b8355 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -733,6 +733,8 @@ export type IContextProvider, TContextName export interface IHttpFetchError extends Error { // (undocumented) readonly body?: any; + // (undocumented) + readonly name: string; // @deprecated (undocumented) readonly req: Request; // (undocumented) From 009d350351164d116c8e5eb6e6a310172d934b99 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 14 Feb 2020 09:28:35 +0100 Subject: [PATCH 06/27] [Discover] Improve functional test of context (#57575) By converting adding filters to a sequential mode, the test of clicking on a context link in the discover table should no longer be flaky --- test/functional/apps/context/_discover_navigation.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index aabce6baa8783..b906296037888 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -39,12 +39,10 @@ export default function({ getService, getPageObjects }) { await Promise.all( TEST_COLUMN_NAMES.map(columnName => PageObjects.discover.clickFieldListItemAdd(columnName)) ); - await Promise.all( - TEST_FILTER_COLUMN_NAMES.map(async ([columnName, value]) => { - await PageObjects.discover.clickFieldListItem(columnName); - await PageObjects.discover.clickFieldListPlusFilter(columnName, value); - }) - ); + for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { + await PageObjects.discover.clickFieldListItem(columnName); + await PageObjects.discover.clickFieldListPlusFilter(columnName, value); + } }); it('should open the context view with the selected document as anchor', async function() { From 104d4abecf919e5c1ca7f57ef87ca7100b5f8b1f Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 14 Feb 2020 11:10:41 +0100 Subject: [PATCH 07/27] Migrate vega and graph configs to new platform (#57011) --- docs/setup/settings.asciidoc | 2 +- .../core_plugins/vis_type_vega/index.ts | 7 ---- .../public/__tests__/vega_visualization.js | 7 ++++ .../vis_type_vega/public/legacy.ts | 5 +-- .../vis_type_vega/public/plugin.ts | 6 ++- .../ui/public/new_platform/new_platform.ts | 2 + src/plugins/vis_type_vega/config.ts | 27 +++++++++++++ src/plugins/vis_type_vega/kibana.json | 6 +++ src/plugins/vis_type_vega/public/index.ts | 38 +++++++++++++++++++ src/plugins/vis_type_vega/server/index.ts | 38 +++++++++++++++++++ x-pack/legacy/plugins/graph/index.ts | 8 ---- x-pack/legacy/plugins/graph/public/index.ts | 2 + x-pack/legacy/plugins/graph/public/plugin.ts | 10 ++--- x-pack/plugins/graph/config.ts | 23 +++++++++++ x-pack/plugins/graph/public/index.ts | 7 +++- x-pack/plugins/graph/public/plugin.ts | 18 ++++++++- x-pack/plugins/graph/server/index.ts | 11 ++++++ 17 files changed, 189 insertions(+), 28 deletions(-) create mode 100644 src/plugins/vis_type_vega/config.ts create mode 100644 src/plugins/vis_type_vega/kibana.json create mode 100644 src/plugins/vis_type_vega/public/index.ts create mode 100644 src/plugins/vis_type_vega/server/index.ts create mode 100644 x-pack/plugins/graph/config.ts diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 4eddb1779a26a..c1f06aff722b5 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -447,7 +447,7 @@ us improve your user experience. Your data is never shared with anyone. Set to `false` to disable telemetry capabilities entirely. You can alternatively opt out through the *Advanced Settings* in {kib}. -`vega.enableExternalUrls:`:: *Default: false* Set this value to true to allow Vega to use any URL to access external data sources and images. If false, Vega can only get data from Elasticsearch. +`vis_type_vega.enableExternalUrls:`:: *Default: false* Set this value to true to allow Vega to use any URL to access external data sources and images. If false, Vega can only get data from Elasticsearch. `xpack.license_management.enabled`:: *Default: true* Set this value to false to disable the License Management user interface. diff --git a/src/legacy/core_plugins/vis_type_vega/index.ts b/src/legacy/core_plugins/vis_type_vega/index.ts index 52c253c6ac0b5..ccef24f8f9746 100644 --- a/src/legacy/core_plugins/vis_type_vega/index.ts +++ b/src/legacy/core_plugins/vis_type_vega/index.ts @@ -39,17 +39,10 @@ const vegaPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPlugin return { emsTileLayerId: mapConfig.emsTileLayerId, - enableExternalUrls: serverConfig.get('vega.enableExternalUrls'), }; }, }, init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - enableExternalUrls: Joi.boolean().default(false), - }).default(); - }, } as Legacy.PluginSpecOptions); // eslint-disable-next-line import/no-default-export diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js index b2ad45b5d7b6d..868e5729bd494 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js +++ b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js @@ -47,6 +47,7 @@ import { createVegaTypeDefinition } from '../vega_type'; // this test has to be migrated to the newly created integration test environment. // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { npStart } from 'ui/new_platform'; +import { setInjectedVars } from '../services'; const THRESHOLD = 0.1; const PIXEL_DIFF = 30; @@ -60,6 +61,12 @@ describe('VegaVisualizations', () => { let vegaVisualizationDependencies; let visRegComplete = false; + setInjectedVars({ + emsTileLayerId: {}, + enableExternalUrls: true, + esShardTimeout: 10000, + }); + beforeEach(ngMock.module('kibana')); beforeEach( ngMock.inject((Private, $injector) => { diff --git a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts b/src/legacy/core_plugins/vis_type_vega/public/legacy.ts index a7928c7d65e81..38ce706ed13ef 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/legacy.ts @@ -26,9 +26,8 @@ import { LegacyDependenciesPlugin } from './shim'; import { plugin } from '.'; const setupPlugins: Readonly = { - expressions: npSetup.plugins.expressions, + ...npSetup.plugins, visualizations: visualizationsSetup, - data: npSetup.plugins.data, // Temporary solution // It will be removed when all dependent services are migrated to the new platform. @@ -36,7 +35,7 @@ const setupPlugins: Readonly = { }; const startPlugins: Readonly = { - data: npStart.plugins.data, + ...npStart.plugins, }; const pluginInstance = plugin({} as PluginInitializerContext); diff --git a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts index 9721de9848cfc..b354433330caf 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts @@ -31,6 +31,7 @@ import { import { createVegaFn } from './vega_fn'; import { createVegaTypeDefinition } from './vega_type'; +import { VisTypeVegaSetup } from '../../../../plugins/vis_type_vega/public'; /** @internal */ export interface VegaVisualizationDependencies extends LegacyDependenciesPluginSetup { @@ -45,6 +46,7 @@ export interface VegaPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; data: ReturnType; + visTypeVega: VisTypeVegaSetup; __LEGACY: LegacyDependenciesPlugin; } @@ -63,11 +65,11 @@ export class VegaPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { data, expressions, visualizations, __LEGACY }: VegaPluginSetupDependencies + { data, expressions, visualizations, visTypeVega, __LEGACY }: VegaPluginSetupDependencies ) { setInjectedVars({ + enableExternalUrls: visTypeVega.config.enableExternalUrls, esShardTimeout: core.injectedMetadata.getInjectedVar('esShardTimeout') as number, - enableExternalUrls: core.injectedMetadata.getInjectedVar('enableExternalUrls') as boolean, emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), }); setUISettings(core.uiSettings); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index ff8fc9b07879c..b7994c7f68afb 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -44,6 +44,7 @@ import { NavigationPublicPluginSetup, NavigationPublicPluginStart, } from '../../../../plugins/navigation/public'; +import { VisTypeVegaSetup } from '../../../../plugins/vis_type_vega/public'; export interface PluginsSetup { bfetch: BfetchPublicSetup; @@ -61,6 +62,7 @@ export interface PluginsSetup { usageCollection: UsageCollectionSetup; advancedSettings: AdvancedSettingsSetup; management: ManagementSetup; + visTypeVega: VisTypeVegaSetup; telemetry?: TelemetryPluginSetup; } diff --git a/src/plugins/vis_type_vega/config.ts b/src/plugins/vis_type_vega/config.ts new file mode 100644 index 0000000000000..c03e86c0a3569 --- /dev/null +++ b/src/plugins/vis_type_vega/config.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + enableExternalUrls: schema.boolean({ defaultValue: false }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_type_vega/kibana.json new file mode 100644 index 0000000000000..6bfd6c9536df4 --- /dev/null +++ b/src/plugins/vis_type_vega/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "visTypeVega", + "version": "kibana", + "server": true, + "ui": true +} diff --git a/src/plugins/vis_type_vega/public/index.ts b/src/plugins/vis_type_vega/public/index.ts new file mode 100644 index 0000000000000..71f3474f8217e --- /dev/null +++ b/src/plugins/vis_type_vega/public/index.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from 'kibana/public'; +import { ConfigSchema } from '../config'; + +export const plugin = (initializerContext: PluginInitializerContext) => ({ + setup() { + return { + /** + * The configuration is temporarily exposed to allow the legacy vega plugin to consume + * the setting. Once the vega plugin is migrated completely, this will become an implementation + * detail. + * @deprecated + */ + config: initializerContext.config.get(), + }; + }, + start() {}, +}); + +export type VisTypeVegaSetup = ReturnType['setup']>; diff --git a/src/plugins/vis_type_vega/server/index.ts b/src/plugins/vis_type_vega/server/index.ts new file mode 100644 index 0000000000000..4c809ff3c5a93 --- /dev/null +++ b/src/plugins/vis_type_vega/server/index.ts @@ -0,0 +1,38 @@ +/* + * 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 { PluginConfigDescriptor } from 'kibana/server'; + +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + enableExternalUrls: true, + }, + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('vega.enableExternalUrls', 'vis_type_vega.enableExternalUrls'), + renameFromRoot('vega.enabled', 'vis_type_vega.enabled'), + ], +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/x-pack/legacy/plugins/graph/index.ts b/x-pack/legacy/plugins/graph/index.ts index 143d07cfdbd57..b2d6fd3957d64 100644 --- a/x-pack/legacy/plugins/graph/index.ts +++ b/x-pack/legacy/plugins/graph/index.ts @@ -44,14 +44,6 @@ export const graph: LegacyPluginInitializer = kibana => { }, init(server) { - server.injectUiAppVars('graph', () => { - const config = server.config(); - return { - graphSavePolicy: config.get('xpack.graph.savePolicy'), - canEditDrillDownUrls: config.get('xpack.graph.canEditDrillDownUrls'), - }; - }); - server.plugins.xpack_main.registerFeature({ id: 'graph', name: i18n.translate('xpack.graph.featureRegistry.graphFeatureName', { diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts index d9854acb9332c..fb60a66fb28cc 100644 --- a/x-pack/legacy/plugins/graph/public/index.ts +++ b/x-pack/legacy/plugins/graph/public/index.ts @@ -7,9 +7,11 @@ import { npSetup, npStart } from 'ui/new_platform'; import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; import { GraphPlugin } from './plugin'; +import { GraphSetup } from '../../../../plugins/graph/public'; type XpackNpSetupDeps = typeof npSetup.plugins & { licensing: LicensingPluginSetup; + graph: GraphSetup; }; (async () => { diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts index b4ca4bf423181..d0797e716d84e 100644 --- a/x-pack/legacy/plugins/graph/public/plugin.ts +++ b/x-pack/legacy/plugins/graph/public/plugin.ts @@ -11,6 +11,7 @@ import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../src/plugins/navigation/public'; import { initAngularBootstrap } from '../../../../../src/plugins/kibana_legacy/public'; +import { GraphSetup } from '../../../../plugins/graph/public'; export interface GraphPluginStartDependencies { npData: ReturnType; @@ -19,6 +20,7 @@ export interface GraphPluginStartDependencies { export interface GraphPluginSetupDependencies { licensing: LicensingPluginSetup; + graph: GraphSetup; } export class GraphPlugin implements Plugin { @@ -26,7 +28,7 @@ export class GraphPlugin implements Plugin { private npDataStart: ReturnType | null = null; private savedObjectsClient: SavedObjectsClientContract | null = null; - setup(core: CoreSetup, { licensing }: GraphPluginSetupDependencies) { + setup(core: CoreSetup, { licensing, graph }: GraphPluginSetupDependencies) { initAngularBootstrap(); core.application.register({ id: 'graph', @@ -41,10 +43,8 @@ export class GraphPlugin implements Plugin { savedObjectsClient: this.savedObjectsClient!, addBasePath: core.http.basePath.prepend, getBasePath: core.http.basePath.get, - canEditDrillDownUrls: core.injectedMetadata.getInjectedVar( - 'canEditDrillDownUrls' - ) as boolean, - graphSavePolicy: core.injectedMetadata.getInjectedVar('graphSavePolicy') as string, + canEditDrillDownUrls: graph.config.canEditDrillDownUrls, + graphSavePolicy: graph.config.savePolicy, storage: new Storage(window.localStorage), capabilities: contextCore.application.capabilities.graph, coreStart: contextCore, diff --git a/x-pack/plugins/graph/config.ts b/x-pack/plugins/graph/config.ts new file mode 100644 index 0000000000000..3838d6ca34ba4 --- /dev/null +++ b/x-pack/plugins/graph/config.ts @@ -0,0 +1,23 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + savePolicy: schema.oneOf( + [ + schema.literal('none'), + schema.literal('config'), + schema.literal('configAndData'), + schema.literal('configAndDataWithConsent'), + ], + { defaultValue: 'configAndData' } + ), + canEditDrillDownUrls: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf; diff --git a/x-pack/plugins/graph/public/index.ts b/x-pack/plugins/graph/public/index.ts index ac9ca960c0c7f..7b2ce67631713 100644 --- a/x-pack/plugins/graph/public/index.ts +++ b/x-pack/plugins/graph/public/index.ts @@ -4,6 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PluginInitializerContext } from 'kibana/public'; import { GraphPlugin } from './plugin'; +import { ConfigSchema } from '../config'; -export const plugin = () => new GraphPlugin(); +export const plugin = (initializerContext: PluginInitializerContext) => + new GraphPlugin(initializerContext); + +export { GraphSetup } from './plugin'; diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts index c0cec14e04d61..e911b400349f8 100644 --- a/x-pack/plugins/graph/public/plugin.ts +++ b/x-pack/plugins/graph/public/plugin.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart } from 'kibana/public'; import { Plugin } from 'src/core/public'; +import { PluginInitializerContext } from 'kibana/public'; import { toggleNavLink } from './services/toggle_nav_link'; import { LicensingPluginSetup } from '../../licensing/public'; import { checkLicense } from '../common/check_license'; @@ -14,15 +15,18 @@ import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; +import { ConfigSchema } from '../config'; export interface GraphPluginSetupDependencies { licensing: LicensingPluginSetup; home?: HomePublicPluginSetup; } -export class GraphPlugin implements Plugin { +export class GraphPlugin implements Plugin<{ config: Readonly }, void> { private licensing: LicensingPluginSetup | null = null; + constructor(private initializerContext: PluginInitializerContext) {} + setup(core: CoreSetup, { licensing, home }: GraphPluginSetupDependencies) { this.licensing = licensing; @@ -39,6 +43,16 @@ export class GraphPlugin implements Plugin { category: FeatureCatalogueCategory.DATA, }); } + + return { + /** + * The configuration is temporarily exposed to allow the legacy graph plugin to consume + * the setting. Once the graph plugin is migrated completely, this will become an implementation + * detail. + * @deprecated + */ + config: this.initializerContext.config.get(), + }; } start(core: CoreStart) { @@ -52,3 +66,5 @@ export class GraphPlugin implements Plugin { stop() {} } + +export type GraphSetup = ReturnType; diff --git a/x-pack/plugins/graph/server/index.ts b/x-pack/plugins/graph/server/index.ts index ac9ca960c0c7f..a5900e1778b90 100644 --- a/x-pack/plugins/graph/server/index.ts +++ b/x-pack/plugins/graph/server/index.ts @@ -4,6 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PluginConfigDescriptor } from 'kibana/server'; + +import { configSchema, ConfigSchema } from '../config'; import { GraphPlugin } from './plugin'; export const plugin = () => new GraphPlugin(); + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + canEditDrillDownUrls: true, + savePolicy: true, + }, + schema: configSchema, +}; From 273fa4371aae8002d42ec61e2481d8d3028ed883 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 14 Feb 2020 11:11:27 +0100 Subject: [PATCH 08/27] TSVB validation: Allow empty strings for number inputs (#57294) --- .../server/routes/post_vis_schema.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts b/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts index 3aca50b5b4710..7893ad456e83b 100644 --- a/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts +++ b/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts @@ -32,11 +32,11 @@ const numberIntegerRequired = Joi.number() .integer() .required(); const numberOptional = Joi.number().optional(); -const numberRequired = Joi.number().required(); const queryObject = Joi.object({ language: Joi.string().allow(''), query: Joi.string().allow(''), }); +const numberOptionalOrEmptyString = Joi.alternatives(numberOptional, Joi.string().valid('')); const annotationsItems = Joi.object({ color: stringOptionalNullable, @@ -74,6 +74,16 @@ const metricsItems = Joi.object({ numerator: stringOptionalNullable, denominator: stringOptionalNullable, sigma: stringOptionalNullable, + unit: stringOptionalNullable, + model_type: stringOptionalNullable, + mode: stringOptionalNullable, + lag: numberOptional, + alpha: numberOptional, + beta: numberOptional, + gamma: numberOptional, + period: numberOptional, + multiplicative: Joi.boolean(), + window: numberOptional, function: stringOptionalNullable, script: stringOptionalNullable, variables: Joi.array() @@ -121,7 +131,7 @@ const seriesItems = Joi.object({ }) ) .optional(), - fill: numberOptional, + fill: numberOptionalOrEmptyString, filter: Joi.object({ query: stringRequired, language: stringOptionalNullable, @@ -131,11 +141,11 @@ const seriesItems = Joi.object({ hidden: Joi.boolean().optional(), id: stringRequired, label: stringOptionalNullable, - line_width: numberOptional, + line_width: numberOptionalOrEmptyString, metrics: Joi.array().items(metricsItems), offset_time: stringOptionalNullable, override_index_pattern: numberOptional, - point_size: numberRequired, + point_size: numberOptionalOrEmptyString, separate_axis: numberIntegerOptional, seperate_axis: numberIntegerOptional, series_index_pattern: stringOptionalNullable, From 1aadf3e021da3a22272b6e69f6438a8ba141e9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 14 Feb 2020 10:35:33 +0000 Subject: [PATCH 09/27] [APM] Custom actions: Settings list page for managing custom actions (#56853) * creating customize ui and empty prompt * new custome ui page, with create custom action flyout * removing import * removing import * fixing translations * fixing pr comments * fixing pr comments * fixing translations * fixing labels * renaming * fixing translations --- .../app/Main/route_config/index.tsx | 14 + .../app/Main/route_config/route_names.tsx | 3 +- .../AddEditFlyout/index.tsx | 8 +- .../CustomActionsFlyout/SettingsSection.tsx | 81 +++ .../CustomActionsFlyout/index.tsx | 109 ++++ .../CustomActionsOverview/EmptyPrompt.tsx | 52 ++ .../CustomActionsOverview/Title.tsx | 40 ++ .../__test__/CustomActions.test.tsx | 33 ++ .../CustomActionsOverview/index.tsx | 75 +++ .../app/Settings/CustomizeUI/index.tsx | 26 + .../public/components/app/Settings/index.tsx | 22 +- .../ServiceForm/index.tsx} | 40 +- .../translations/translations/ja-JP.json | 533 +++++++++--------- .../translations/translations/zh-CN.json | 533 +++++++++--------- 14 files changed, 1000 insertions(+), 569 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/SettingsSection.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/index.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/EmptyPrompt.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/Title.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/__test__/CustomActions.test.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/index.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx rename x-pack/legacy/plugins/apm/public/components/{app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx => shared/ServiceForm/index.tsx} (76%) 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 2f7df3c5a4acd..3be096d9db2bc 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 0ae7a948be4e1..db57e8356f39b 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 e1cb07be3d378..7243a86404f04 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 0000000000000..8cb604d367549 --- /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 0000000000000..d04cdd62c303b --- /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 0000000000000..f39e4b307b24c --- /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 0000000000000..d7f90e0919733 --- /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 0000000000000..970de66c64a9a --- /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 0000000000000..ae2972f251fc2 --- /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 0000000000000..17a4b2f847679 --- /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 f3be5abe4d48b..eef386731c5c3 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 513dfceaa3ae2..58a203bded715 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/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6bcf61b53fd5f..79b826bc4524f 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 25382221716dd..ce1c713adc4bc 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": "选择选项", From f87053e0b510a434f21dd18947a0d13025ebcaa0 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet <pierre.gayvallet@elastic.co> Date: Fri, 14 Feb 2020 12:23:53 +0100 Subject: [PATCH 10/27] Use app id instead of pluginId to generate navlink from legacy apps (#57542) * properly use app id instead of pluginId to generate navlink * extract convertToNavLink, add more tests * use distinct mapping methods * fix linkToLastSubUrl default value * nits & doc --- .../__snapshots__/get_nav_links.test.ts.snap | 56 ++++ .../plugins/find_legacy_plugin_specs.ts | 68 +---- .../legacy/plugins/get_nav_links.test.ts | 283 ++++++++++++++++++ .../server/legacy/plugins/get_nav_links.ts | 82 +++++ src/core/server/legacy/types.ts | 14 +- src/core/server/server.api.md | 10 +- 6 files changed, 437 insertions(+), 76 deletions(-) create mode 100644 src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap create mode 100644 src/core/server/legacy/plugins/get_nav_links.test.ts create mode 100644 src/core/server/legacy/plugins/get_nav_links.ts 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 0000000000000..c1b7164908ed6 --- /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 1c6ab91a39279..44f02f0c90d4e 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<LegacyNavLink>(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 0000000000000..dcb19020f769e --- /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 0000000000000..067fb204ca7f3 --- /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 d51058ca561c6..0c1a7730f92a7 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<string, unknown> & ChromeNavLink; +export type LegacyNavLinkSpec = Partial<LegacyNavLink> & { + 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<LegacyNavLink> & { + pluginId?: string; + listed?: boolean; +}; /** * @internal diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 96a93033c7408..ad1907df571fb 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 From f8b06e3726ba54a52415dfa7cfb5e14471235f6a Mon Sep 17 00:00:00 2001 From: igoristic <igor.zaytsev.dev@gmail.com> Date: Fri, 14 Feb 2020 06:29:49 -0500 Subject: [PATCH 11/27] [Monitoring] Fixed logs timezone (#57611) * Fixed logs timezone * Passing tz as a param * Fixed tests Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> --- .../legacy/plugins/monitoring/common/formatting.js | 12 +++++++----- .../monitoring/public/components/alerts/alerts.js | 10 +++++++--- .../public/components/chart/get_chart_options.js | 2 +- .../components/cluster/overview/alerts_panel.js | 6 +++++- .../ccr_shard/__snapshots__/ccr_shard.test.js.snap | 2 +- .../components/elasticsearch/ccr_shard/ccr_shard.js | 5 ++++- .../elasticsearch/ccr_shard/ccr_shard.test.js | 1 + .../elasticsearch/shard_activity/parse_props.js | 5 ++++- .../monitoring/public/components/logs/logs.js | 10 ++++++++-- .../monitoring/public/views/license/controller.js | 4 +++- 10 files changed, 41 insertions(+), 16 deletions(-) diff --git a/x-pack/legacy/plugins/monitoring/common/formatting.js b/x-pack/legacy/plugins/monitoring/common/formatting.js index a3b3ce07c8c76..ed5d68f942dfd 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 4c2f3b027bc8a..11fcef73a4b97 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 6f26abeadb3a0..661d51e068201 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 a8001638f4399..8455fb8cf3088 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 366c23135cc76..e55f9c84b51fe 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 68c87b386da49..af0ff323b7ba8 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 17caa8429a275..b950c2ca0a6d2 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 692025631f3b8..133b520947b1b 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 926f5cdda26a7..744ebb5a7ceb4 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 dcd3ca76ceffd..ce6e9c8fb74cd 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 From 979c1d24fa905a88e03e0b7eefb586e6ac1c74fa Mon Sep 17 00:00:00 2001 From: Dario Gieselaar <dario.gieselaar@elastic.co> Date: Fri, 14 Feb 2020 13:06:57 +0100 Subject: [PATCH 12/27] [APM] Add `xpack.apm.enabled` key to config schema (#57539) Closes #57418. --- x-pack/plugins/apm/server/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index b0e10d245e0b9..d936e2a467f52 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({ From c1cf4896a54c73572769c558c1d924ffb6cef085 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger <walter@elastic.co> Date: Fri, 14 Feb 2020 13:07:54 +0100 Subject: [PATCH 13/27] [ML] Fix brush visibility. (#57564) Fixes brush visibility. The brush will no longer be hidden if it covers the full available timespan. Removes all code that was earlier used to manage brush visibility. --- .../timeseries_chart/timeseries_chart.js | 87 ++++++------------- 1 file changed, 28 insertions(+), 59 deletions(-) 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 3c639239757db..4d7d095321611 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 @@ -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) { From 7f71c787bddc17aa3c6322d0a0b70dda01135d96 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens <jloleysens@gmail.com> Date: Fri, 14 Feb 2020 13:56:52 +0100 Subject: [PATCH 14/27] [Console] Fix performance bottleneck for large JSON payloads (#57668) * Fix Console performance bug for large request bodies The legacy_core_editor implemenation was calculating the current editor line count by .split('\n').length on the entire buffer which was very inefficient in a tight loop. This caused a performance regression. Now we use the cached line count provided by the underlying editor implementation. * Fix performance regression inside of ace token_provider implementation * Clean up another unnecessary use of getValue().split(..).length. Probably was not a performance issue, just taking unnecessary steps. Not sure that this function is even being used. --- .../__tests__/input.test.js | 568 ++++++++++++++++++ .../__tests__/input_tokenization.test.js | 559 ----------------- .../legacy_core_editor/legacy_core_editor.ts | 5 +- .../models/sense_editor/sense_editor.ts | 2 +- .../lib/ace_token_provider/token_provider.ts | 5 +- .../console/public/types/core_editor.ts | 4 + 6 files changed, 580 insertions(+), 563 deletions(-) create mode 100644 src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/__tests__/input_tokenization.test.js 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 0000000000000..a68a2b3939864 --- /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 = `<div> + <div id="ConAppEditor" /> + <div id="ConAppEditorActions" /> + <div id="ConCopyAsCurl" /> + </div>`; + + 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 019b3c1d0538a..0000000000000 --- 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 = `<div> - <div id="ConAppEditor" /> - <div id="ConAppEditorActions" /> - <div id="ConCopyAsCurl" /> - </div>`; - - 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 19a86648d6dd3..47947e985092b 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 9679eaa2884ce..1271f167c6cc1 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 761eb1d206cfe..134ab6c0e82d5 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 8de4c78333fee..79dc3ca74200b 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; From 098a55a83cd4e998f1622a303200ecf47b51a6a3 Mon Sep 17 00:00:00 2001 From: Anton Dosov <anton.dosov@elastic.co> Date: Fri, 14 Feb 2020 14:07:00 +0100 Subject: [PATCH 15/27] unskip replace flyout test (#57093) Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> --- test/functional/apps/dashboard/panel_controls.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/functional/apps/dashboard/panel_controls.js b/test/functional/apps/dashboard/panel_controls.js index 5ec6cf3389c4e..f30f58913bd97 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(); From 948bfa9950fb65ed07f5b42f53a9e78e2e23709b Mon Sep 17 00:00:00 2001 From: Joe Reuter <johannes.reuter@elastic.co> Date: Fri, 14 Feb 2020 14:11:18 +0100 Subject: [PATCH 16/27] Clean up shims of Graph, Home, Dashboard, Visualize (#57331) --- src/legacy/core_plugins/kibana/index.js | 2 +- .../kibana/public/dashboard/index.ts | 2 +- .../kibana/public/dashboard/legacy.ts | 14 +--- .../public/dashboard/np_ready/application.ts | 20 +++-- .../dashboard/np_ready/dashboard_app.tsx | 2 +- .../np_ready/dashboard_app_controller.tsx | 21 ++--- .../public/dashboard/np_ready/legacy_app.js | 59 +++++++------- .../kibana/public/dashboard/plugin.ts | 68 ++++++++-------- .../core_plugins/kibana/public/home/index.ts | 16 ++-- .../kibana/public/home/kibana_services.ts | 16 +--- .../tutorial/replace_template_strings.js | 4 +- .../core_plugins/kibana/public/home/plugin.ts | 48 ++++-------- .../kibana/public/visualize/index.ts | 2 +- .../public/visualize/kibana_services.ts | 8 +- .../kibana/public/visualize/legacy.ts | 28 ++++--- .../public/visualize/np_ready/application.ts | 8 +- .../visualize/np_ready/editor/editor.js | 2 +- .../np_ready/editor/visualization.js | 2 +- .../np_ready/editor/visualization_editor.js | 2 +- .../public/visualize/np_ready/types.d.ts | 2 +- .../kibana/public/visualize/plugin.ts | 35 +++++---- .../public/default_editor.tsx | 6 +- .../ui/public/legacy_compat/__tests__/xsrf.js | 13 +--- .../kibana_legacy/common/kbn_base_url.ts | 20 +++++ .../public/angular/angular_config.tsx | 77 +++++++++++-------- src/plugins/kibana_legacy/public/index.ts | 1 + src/plugins/kibana_legacy/public/mocks.ts | 5 ++ src/plugins/kibana_legacy/public/plugin.ts | 10 +++ src/plugins/kibana_legacy/server/index.ts | 2 + x-pack/legacy/plugins/graph/public/plugin.ts | 23 ++++-- 30 files changed, 270 insertions(+), 248 deletions(-) create mode 100644 src/plugins/kibana_legacy/common/kbn_base_url.ts diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 36563ba8cbe45..ea81193c1dd0a 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 d0157882689d3..5b9fb8c0b6360 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 9c13337a71126..cedb6fbc9b5ef 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 e608eb7b7f48c..cc104c1a931d0 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 f94acf2dc1991..c0a0693431295 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 3f9343ededd13..465203be0d34c 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 b0f70b7a0c68f..ce9cc85be57b2 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 09ae49f2305fd..7d330676e79ed 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<AngularRenderedAppUpdater>(() => ({})); 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 768e1a96de935..74b6da33c6542 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 57696d874cc40..6cb1531be6b5b 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 daf996444eb3c..c7e623657bf71 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 5cc7c9c11dd2f..75e7cc2e453be 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 83b820a8e3134..c3ae39d9fde25 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 428e6cb225710..6082fb8428ac3 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 2d615e3132b01..bc2d700f6c6a1 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 44e7e9c2a7413..3d5fd6605f56b 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 46ae45c3a5fa2..27fb9b63843c4 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 18a60f7c3c10b..502bd6e56fb1f 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 b2386f83b252c..8032152f88173 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 17be5e4051b12..524bc4b3196b7 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 ce93fe7c2d578..16715677d1e20 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 48a1a6f9d2121..32ea71c0bc005 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 fc12a18d72823..3ca836e23881a 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/kibana_legacy/common/kbn_base_url.ts b/src/plugins/kibana_legacy/common/kbn_base_url.ts new file mode 100644 index 0000000000000..69711626750ea --- /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 9a33cff82ed63..67d62cab7409b 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<EnvironmentMode>; + packageInfo: Readonly<PackageInfo>; + }; + }, 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 19833d638fe4c..18f01854de259 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 aab3ab315f0c6..8e9a05b186191 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<ReturnType<KibanaLegacyPlugin['setup']>>; @@ -28,6 +29,10 @@ const createSetupContract = (): Setup => ({ config: { defaultAppId: 'home', }, + env: {} as { + mode: Readonly<EnvironmentMode>; + packageInfo: Readonly<PackageInfo>; + }, }); const createStartContract = (): Start => ({ diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index 86e56c44646c0..2ad620f355848 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 4d0fe8364a66c..98c754795e947 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<ConfigSchema> = { ], }; +export { kbnBaseUrl } from '../common/kbn_base_url'; + class Plugin { public setup(core: CoreSetup) {} diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts index d0797e716d84e..4ccaf6b5dfa27 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, }); }, }); From 0adda9d2707a42965f06aebe1d3ea039bc6caf0e Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Fri, 14 Feb 2020 13:44:18 +0000 Subject: [PATCH 17/27] [SIEM] Remove additional props for matrix histogram (#54979) * update DNS histogram * fix indent * hide dropdown if only one option provided * update DNS histogram * fix types * wip * isolate matrix histogram on server side * fix type for graphql * fix types * fix tests * fix types * fix types * add unit test * add unit test * fix types * split histogram configs to an object * an idea to simplify more * add unit test * remove updateDateRange passing down to matrixHistogram * change histogramType to enum * handle the case which config not available * fix review * fix review II * fix parser for dns histogram * revert change * isolate parsers * fix unit Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> --- .../alerts_viewer/histogram_configs.ts | 31 + .../public/components/alerts_viewer/index.tsx | 56 +- .../public/components/alerts_viewer/types.ts | 9 +- .../siem/public/components/charts/common.tsx | 6 +- .../__snapshots__/index.test.tsx.snap | 5 + .../matrix_histogram/index.test.tsx | 96 ++- .../components/matrix_histogram/index.tsx | 126 ++- .../components/matrix_histogram/types.ts | 109 +-- .../components/matrix_histogram/utils.test.ts | 139 ++++ .../components/matrix_histogram/utils.ts | 26 +- .../histogram_configs.ts | 31 + .../anomalies_query_tab_body/index.tsx | 25 +- .../matrix_histogram/index.gql_query.ts | 81 +- .../matrix_histogram/index.test.tsx | 154 ++++ .../matrix_histogram/{utils.ts => index.ts} | 74 +- .../containers/matrix_histogram/index.tsx | 66 -- .../siem/public/graphql/introspection.json | 738 ++++++------------ .../plugins/siem/public/graphql/types.ts | 267 ++----- .../detection_engine/detection_engine.tsx | 1 - .../plugins/siem/public/pages/hosts/hosts.tsx | 3 +- .../siem/public/pages/hosts/hosts_tabs.tsx | 3 - .../authentications_query_tab_body.tsx | 29 +- .../navigation/events_query_tab_body.tsx | 34 +- .../plugins/siem/public/pages/hosts/types.ts | 2 +- .../network/navigation/dns_query_tab_body.tsx | 41 +- .../network/navigation/network_routes.tsx | 7 - .../public/pages/network/navigation/types.ts | 2 - .../overview/alerts_by_category/index.tsx | 53 +- .../overview/events_by_dataset/index.tsx | 57 +- .../siem/public/pages/overview/overview.tsx | 2 - .../siem/server/graphql/alerts/index.ts | 8 - .../siem/server/graphql/alerts/resolvers.ts | 39 - .../server/graphql/anomalies/schema.gql.ts | 24 - .../graphql/authentications/resolvers.ts | 16 +- .../graphql/authentications/schema.gql.ts | 12 - .../siem/server/graphql/events/resolvers.ts | 15 - .../siem/server/graphql/events/schema.gql.ts | 18 - .../plugins/siem/server/graphql/index.ts | 6 +- .../{anomalies => matrix_histogram}/index.ts | 4 +- .../resolvers.ts | 22 +- .../schema.gql.ts | 25 +- .../siem/server/graphql/network/resolvers.ts | 14 +- .../plugins/siem/server/graphql/types.ts | 457 ++++------- .../legacy/plugins/siem/server/init_server.ts | 6 +- .../lib/alerts/elasticsearch_adapter.ts | 63 -- .../plugins/siem/server/lib/alerts/index.ts | 21 - .../plugins/siem/server/lib/alerts/types.ts | 27 - .../lib/anomalies/elasticsearch_adapter.ts | 64 -- .../siem/server/lib/anomalies/types.ts | 42 - .../authentications/elasticsearch_adapter.ts | 63 +- .../siem/server/lib/authentications/index.ts | 14 +- .../siem/server/lib/authentications/types.ts | 30 +- .../plugins/siem/server/lib/compose/kibana.ts | 7 +- .../lib/events/elasticsearch_adapter.ts | 63 +- .../plugins/siem/server/lib/events/index.ts | 10 +- .../plugins/siem/server/lib/events/types.ts | 12 +- .../siem/server/lib/framework/types.ts | 4 +- .../matrix_histogram/elasticsearch_adapter.ts | 81 ++ .../elasticseatch_adapter.test.ts | 8 +- .../{anomalies => matrix_histogram}/index.ts | 14 +- .../lib/{alerts => matrix_histogram}/mock.ts | 5 +- .../query.anomalies_over_time.dsl.ts | 0 .../query.authentications_over_time.dsl.ts | 0 .../query.events_over_time.dsl.ts | 0 .../query_alerts.dsl.ts} | 2 +- .../query_dns_histogram.dsl.ts | 2 +- .../siem/server/lib/matrix_histogram/types.ts | 144 ++++ .../siem/server/lib/matrix_histogram/utils.ts | 48 ++ .../lib/network/elasticsearch_adapter.ts | 45 +- .../plugins/siem/server/lib/network/index.ts | 14 +- .../plugins/siem/server/lib/network/types.ts | 11 +- .../legacy/plugins/siem/server/lib/types.ts | 6 +- 72 files changed, 1495 insertions(+), 2244 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/alerts_viewer/histogram_configs.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.test.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.test.tsx rename x-pack/legacy/plugins/siem/public/containers/matrix_histogram/{utils.ts => index.ts} (61%) delete mode 100644 x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.tsx delete mode 100644 x-pack/legacy/plugins/siem/server/graphql/alerts/index.ts delete mode 100644 x-pack/legacy/plugins/siem/server/graphql/alerts/resolvers.ts delete mode 100644 x-pack/legacy/plugins/siem/server/graphql/anomalies/schema.gql.ts rename x-pack/legacy/plugins/siem/server/graphql/{anomalies => matrix_histogram}/index.ts (67%) rename x-pack/legacy/plugins/siem/server/graphql/{anomalies => matrix_histogram}/resolvers.ts (55%) rename x-pack/legacy/plugins/siem/server/graphql/{alerts => matrix_histogram}/schema.gql.ts (57%) delete mode 100644 x-pack/legacy/plugins/siem/server/lib/alerts/elasticsearch_adapter.ts delete mode 100644 x-pack/legacy/plugins/siem/server/lib/alerts/index.ts delete mode 100644 x-pack/legacy/plugins/siem/server/lib/alerts/types.ts delete mode 100644 x-pack/legacy/plugins/siem/server/lib/anomalies/elasticsearch_adapter.ts delete mode 100644 x-pack/legacy/plugins/siem/server/lib/anomalies/types.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticsearch_adapter.ts rename x-pack/legacy/plugins/siem/server/lib/{alerts => matrix_histogram}/elasticseatch_adapter.test.ts (86%) rename x-pack/legacy/plugins/siem/server/lib/{anomalies => matrix_histogram}/index.ts (55%) rename x-pack/legacy/plugins/siem/server/lib/{alerts => matrix_histogram}/mock.ts (95%) rename x-pack/legacy/plugins/siem/server/lib/{anomalies => matrix_histogram}/query.anomalies_over_time.dsl.ts (100%) rename x-pack/legacy/plugins/siem/server/lib/{authentications => matrix_histogram}/query.authentications_over_time.dsl.ts (100%) rename x-pack/legacy/plugins/siem/server/lib/{events => matrix_histogram}/query.events_over_time.dsl.ts (100%) rename x-pack/legacy/plugins/siem/server/lib/{alerts/query.dsl.ts => matrix_histogram/query_alerts.dsl.ts} (98%) rename x-pack/legacy/plugins/siem/server/lib/{network => matrix_histogram}/query_dns_histogram.dsl.ts (98%) create mode 100644 x-pack/legacy/plugins/siem/server/lib/matrix_histogram/types.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/matrix_histogram/utils.ts 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 0000000000000..fbcf4c6ed039b --- /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 a8c2f429040ea..587002c24d526 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 e6d6fdf273ec8..a24c66e31e670 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 62f1ac56890ca..03b412f575646 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 0000000000000..0e518e48e2e88 --- /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 a44efed47372d..db5b1f7f03ee3 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 04b988f8270f3..cb9afde899cf8 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 88f8f1ff28fa9..fda4f5d15d95c 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 0000000000000..2c34a307bfded --- /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 95b1cd806cf6c..ccd1b03eb5474 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 0000000000000..f63349d3e573a --- /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 e34832aa88c93..85e19248f2eb5 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 e21d4c6e34ff8..6fb729ca7e9a0 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 0000000000000..06367ab8657a8 --- /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 1df1aec76627c..683d5b68c305b 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 9e0b1579a7b65..0000000000000 --- 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 b356b67b75c7b..9802a5f5bd3bf 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 0103713a8c8a2..3528ee6e13a38 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 8a37461746773..8cfcac8fc862b 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 2e2986fb632b1..06dffcdb220a9 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 9c13fc4ac386e..0b83710a13293 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 a6a0344599842..fb083b7a7da2f 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 0ea82ba53b3a2..cb2c19c642bc4 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 5900937d2108e..e6e2ebb9ac2fe 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 b49849b285d8e..fe456afcc7189 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 acc5d02299f1f..23a619db97ee4 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 b6063a81f31f6..222a99992917d 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 98ae3f30085a9..f71d83558ae9d 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 5b6ad69bcb15d..315aac5fcae9e 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 6f8446a6b1609..8505b91fe1ff5 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/index.ts b/x-pack/legacy/plugins/siem/server/graphql/alerts/index.ts deleted file mode 100644 index f2beae525ed6b..0000000000000 --- a/x-pack/legacy/plugins/siem/server/graphql/alerts/index.ts +++ /dev/null @@ -1,8 +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 { createAlertsResolvers } from './resolvers'; -export { alertsSchema } from './schema.gql'; 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 5a3a50d5c6ec6..0000000000000 --- 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/anomalies/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/anomalies/schema.gql.ts deleted file mode 100644 index a0b834f705696..0000000000000 --- a/x-pack/legacy/plugins/siem/server/graphql/anomalies/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 anomaliesSchema = gql` - type AnomaliesOverTimeData { - inspect: Inspect - matrixHistogramData: [MatrixOverTimeHistogramData!]! - totalCount: Float! - } - - extend type Source { - AnomaliesHistogram( - timerange: TimerangeInput! - filterQuery: String - defaultIndex: [String!]! - stackByField: String - ): AnomaliesOverTimeData! - } -`; 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 ce1c86ac8926c..b66ccd9a111b7 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 4acc72a5b0b6f..20935ce9ed03f 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 335f4c3bf4da3..a9ef6bc682c84 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 9b321d10614fc..3b71977bc0d47 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 60853e2ce7bed..7e25735707893 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/anomalies/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/anomalies/index.ts rename to x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/index.ts index 4bfd6be173105..1460b6022bb13 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/anomalies/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 { createAnomaliesResolvers } from './resolvers'; -export { anomaliesSchema } 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 e7b7a640c58d2..35cebe4777dcf 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/alerts/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/alerts/schema.gql.ts rename to x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/schema.gql.ts index ca91468b1e0f2..deda6dc6e5c1a 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/alerts/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 alertsSchema = gql` - type AlertsOverTimeData { +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 { - AlertsHistogram( + MatrixHistogram( filterQuery: String defaultIndex: [String!]! timerange: TimerangeInput! - stackByField: String - ): AlertsOverTimeData! + 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 06d6b8c516d8b..db15babc42a72 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 c3fd6e9dde286..f42da48f2c1da 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 1f4f1b176497f..6158a33c25cfa 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 cedd781596812..0000000000000 --- 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 9cfb1841edfef..0000000000000 --- 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 67da38e8052d2..0000000000000 --- 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 0955bc69c7c93..0000000000000 --- 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 9fde81da63ec7..0000000000000 --- 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 85008adcd985f..79f13ce4461e5 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 bd5712c105f31..c1b93818943db 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 e1ec871ff4b58..2d2c7ba547c09 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 0ab6f1a8df779..9c46f3320e37e 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 38b95cc5772f2..af6f8314b362a 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 9e2457904f8c0..9c1f87aa3d8bf 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 2da0ff13638e1..3a4a8705f7387 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 9fc78e6fb84fe..7d049d1dcd195 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 0000000000000..f661fe165130e --- /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 210c97892e25c..0b63785d2203b 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 727c45a3bac44..900a6ab619ae0 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 fe0b6673f3191..3e51e926bea87 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 eb82327197543..4963f01d67a4f 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 1ce324e0ffff8..a6c75fe01eb15 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 0000000000000..87ea4b81f5fba --- /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 0000000000000..67568b96fee90 --- /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 4bd980fd2ff80..39babc58ee138 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 cbcd33b753d8a..42ce9f0726ddb 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 b5563f9a2fef1..b7848be097151 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 34a50cf962412..323ced734d24b 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; From cefe28c1f421d8f407a0512741cce1cadbe0bb08 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger <walter@elastic.co> Date: Fri, 14 Feb 2020 14:52:04 +0100 Subject: [PATCH 18/27] [ML] Fix single metric viewer chart resize. (#57578) Fix to trigger a chart update with the correct width when resizing the browser window. Previously after a browser refresh, or opening the view from a bookmarked URL, the chart would not resize until a state change was made to the view (such as moving the zoom slider or altering the time range). --- .../components/timeseries_chart/timeseries_chart.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 4d7d095321611..bafb12de068bb 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(); } From 343bc9c30394d679791dcae56ea821552fdfeea3 Mon Sep 17 00:00:00 2001 From: Spencer <email@spalger.com> Date: Fri, 14 Feb 2020 06:54:24 -0700 Subject: [PATCH 19/27] [kbn/optimizer] Fix windows support (#57592) * [kbn/optimizer] simplify run_workers.ts a smidge * use Path.resolve() to create windows paths from normalized ones Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> --- .../src/optimizer/kibana_platform_plugins.ts | 5 +- .../kbn-optimizer/src/worker/run_worker.ts | 49 ++++++++----------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts index b7e5e12f46a7f..2165878e92ff4 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 d6ca2aa94fb1a..cbec4c3f44c7d 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); + } +); From c965a9efa8490539327a04bb005987c72fb1e58d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= <mikecote@users.noreply.github.com> Date: Fri, 14 Feb 2020 08:55:13 -0500 Subject: [PATCH 20/27] Skip flaky test (#57675) --- .../functional_with_es_ssl/apps/triggers_actions_ui/details.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 95371b5b501f5..6d83e0bbf1df7 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 From a790f61460c3a5da9e0c927d13051e933b0a7f50 Mon Sep 17 00:00:00 2001 From: Robert Austin <robert.austin@elastic.co> Date: Fri, 14 Feb 2020 09:41:22 -0500 Subject: [PATCH 21/27] Resolver: Animate camera, add sidebar (#55590) This PR adds a sidebar navigation. clicking the icons in the nav will focus the camera on the different nodes. There is an animation effect when the camera moves. --- x-pack/plugins/endpoint/package.json | 3 +- .../public/embeddables/resolver/actions.ts | 9 - .../resolver/documentation/camera.md | 26 + .../embeddables/resolver/embeddable.tsx | 10 +- .../public/embeddables/resolver/lib/math.ts | 7 + .../resolver/lib/transformation.ts | 60 +- .../embeddables/resolver/lib/vector2.ts | 37 ++ .../models/process_event_test_helpers.ts | 1 + .../embeddables/resolver/store/actions.ts | 27 + .../resolver/store/camera/action.ts | 65 +- .../resolver/store/camera/animation.test.ts | 193 ++++++ .../camera/inverse_projection_matrix.test.ts | 19 +- .../resolver/store/camera/methods.ts | 36 ++ .../resolver/store/camera/panning.test.ts | 111 ++-- .../store/camera/projection_matrix.test.ts | 16 +- .../resolver/store/camera/reducer.ts | 164 ++--- .../store/camera/scaling_constants.ts | 12 +- .../resolver/store/camera/selectors.ts | 575 ++++++++++++++---- .../resolver/store/camera/zooming.test.ts | 29 +- .../data/__snapshots__/graphing.test.ts.snap | 11 + .../resolver/store/data/selectors.ts | 14 +- .../embeddables/resolver/store/index.ts | 46 +- .../embeddables/resolver/store/methods.ts | 30 + .../embeddables/resolver/store/reducer.ts | 12 +- .../embeddables/resolver/store/selectors.ts | 9 + .../public/embeddables/resolver/types.ts | 125 +++- .../embeddables/resolver/view/edge_line.tsx | 10 +- .../resolver/view/graph_controls.tsx | 43 +- .../embeddables/resolver/view/index.tsx | 177 ++---- .../embeddables/resolver/view/panel.tsx | 165 +++++ .../resolver/view/process_event_dot.tsx | 11 +- .../resolver/view/side_effect_context.ts | 27 + .../resolver/view/side_effect_simulator.ts | 170 ++++++ .../view/use_autoupdating_client_rect.tsx | 43 -- .../resolver/view/use_camera.test.tsx | 197 ++++++ .../embeddables/resolver/view/use_camera.ts | 307 ++++++++++ .../view/use_nonpassive_wheel_handler.tsx | 26 - .../resolver/view/use_resolver_dispatch.ts | 13 + yarn.lock | 5 + 39 files changed, 2226 insertions(+), 615 deletions(-) delete mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/documentation/camera.md create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/animation.test.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/methods.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/view/side_effect_context.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/view/side_effect_simulator.ts delete mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.ts delete mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/view/use_nonpassive_wheel_handler.tsx create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/view/use_resolver_dispatch.ts diff --git a/x-pack/plugins/endpoint/package.json b/x-pack/plugins/endpoint/package.json index 8efd0eab0eee0..25afe2c8442ba 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/embeddables/resolver/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts deleted file mode 100644 index c7f790588a739..0000000000000 --- 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 0000000000000..aeca76fad916f --- /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 9539162f9cfb6..6680ba615e353 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 c59db31c39e82..6bf0fedc84dfe 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 3084ce0eacdb4..bd7d1bf743df8 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 3c0681413305e..898ce6f6bacd2 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 67acdbd253f65..9a6f19adcc101 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 0000000000000..25f196c76a290 --- /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 7d3e64ab34f23..dcc6c2c9c9411 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 0000000000000..795344d8af092 --- /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 41e3bc025f557..000dbb8d52841 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 0000000000000..4afbacb819b1a --- /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 17401a63b5ae8..9a9a5ea1c0cfc 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 e21e3d1001794..e868424d06c94 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 7c4678a4f1dc1..0f6ae1b7d904a 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 93c41fde64f0e..243d8877a8b0d 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 53ffe6dd073fa..226e36f63d788 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 abc113d5999ff..fb38c2f526e0b 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 261ca7e0a7bba..1dc17054b9f47 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 745bd125c151d..75b477dd7c7fc 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 d043453a8e4cd..b17572bbc4ab4 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 0000000000000..8808160c9c631 --- /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 97ab51cbd6dea..20c490b8998f9 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 eb1c1fec36995..4d12e656205fa 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 f2ae9785446f7..6c6936d377dea 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 cdecd3e02bde1..3386ed4a448d5 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 3170f8bdf867e..a1cd003949a22 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 a69504e3a5db1..d71a4d87b7eab 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 0000000000000..c75b73b4bceaf --- /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 5c3a253d619ef..384fbf90ed984 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 0000000000000..ab7f41d815026 --- /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 0000000000000..3e80b6a8459f7 --- /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 5f13995de1c2a..0000000000000 --- 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 0000000000000..85e1d4e694b15 --- /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 0000000000000..54940b8383f7a --- /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 a0738bcf4d14c..0000000000000 --- 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 0000000000000..a993a4ed595e1 --- /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/yarn.lock b/yarn.lock index 1158fce12829e..0a55e3d7c7850 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" From f0cb03e599c1ad9504e35e70da2ca1873bbe1f86 Mon Sep 17 00:00:00 2001 From: Robert Oskamp <robert.oskamp@elastic.co> Date: Fri, 14 Feb 2020 16:08:10 +0100 Subject: [PATCH 22/27] Transform functional tests - disable saved search test --- x-pack/test/functional/apps/transform/creation_saved_search.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 4528a2c84d9de..333a53a98c82b 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'); From 1492d5a372057ae8b2ab9c4e77459ba393448d6c Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Fri, 14 Feb 2020 10:12:34 -0500 Subject: [PATCH 23/27] [Endpoint] Task/basic endpoint list (#55623) * Adds host management list to endpoint security plugin Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> --- x-pack/plugins/endpoint/common/types.ts | 2 +- .../public/applications/endpoint/index.tsx | 21 +-- .../applications/endpoint/store/action.ts | 4 +- .../endpoint/store/endpoint_list/action.ts | 25 --- .../endpoint/store/endpoint_list/index.ts | 10 -- .../endpoint/store/endpoint_list/reducer.ts | 36 ---- .../endpoint/store/endpoint_list/saga.test.ts | 118 ------------- .../endpoint/store/endpoint_list/saga.ts | 26 --- .../endpoint/store/endpoint_list/types.ts | 54 ------ .../applications/endpoint/store/index.ts | 51 +++++- .../endpoint/store/managing/action.ts | 27 +++ .../{endpoint_list => managing}/index.test.ts | 89 ++++------ .../selectors.ts => managing/index.ts} | 6 +- .../store/managing/middleware.test.ts | 80 +++++++++ .../endpoint/store/managing/middleware.ts | 36 ++++ .../endpoint/store/managing/reducer.ts | 55 ++++++ .../endpoint/store/managing/selectors.ts | 17 ++ .../applications/endpoint/store/reducer.ts | 4 +- .../applications/endpoint/store/saga.ts | 18 -- .../public/applications/endpoint/types.ts | 30 +++- .../endpoint/view/managing/hooks.ts | 16 ++ .../endpoint/view/managing/index.tsx | 167 ++++++++++++++++++ .../feature_controls/endpoint_spaces.ts | 2 +- x-pack/test/functional/apps/endpoint/index.ts | 1 + .../functional/apps/endpoint/management.ts | 47 +++++ .../functional/page_objects/endpoint_page.ts | 5 + 26 files changed, 572 insertions(+), 375 deletions(-) delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/action.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/reducer.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/types.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts rename x-pack/plugins/endpoint/public/applications/endpoint/store/{endpoint_list => managing}/index.test.ts (51%) rename x-pack/plugins/endpoint/public/applications/endpoint/store/{endpoint_list/selectors.ts => managing/index.ts} (60%) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts delete mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/managing/hooks.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx create mode 100644 x-pack/test/functional/apps/endpoint/management.ts diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 0128cd3dd6df7..0dc3fc29ca805 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/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 9bea41126d296..a86c647e771d4 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 593041af75c05..04c6cf7fc4634 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 02ec0f9d09035..0000000000000 --- 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 bdf0708457bb0..0000000000000 --- 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 e57d9683e4707..0000000000000 --- 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 6bf946873e179..0000000000000 --- 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 cc156cfa88002..0000000000000 --- 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/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/types.ts deleted file mode 100644 index f2810dd89f857..0000000000000 --- 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 a32f310392ca9..3bbcc3f25a6d8 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 0000000000000..e916dc66c59f0 --- /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 a46653f82ee45..dde0ba1e96a8a 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/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.ts similarity index 60% rename from x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.ts index 6ffcebc3f41aa..f0bfe27c9e30f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.ts @@ -4,6 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EndpointListState } from './types'; - -export const endpointListData = (state: EndpointListState) => state.endpoints; +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 0000000000000..095e49a6c4306 --- /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 0000000000000..ae756caf5aa35 --- /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 0000000000000..bbbbdc4d17ce6 --- /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 0000000000000..3dcb144c2bade --- /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 a9cf6d9980519..7d738c266fae0 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 3b7de79d5443c..0000000000000 --- 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 5f02d36308053..02a7793fc38b0 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 0000000000000..a0720fbd8aeeb --- /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 0000000000000..44b08f25c7653 --- /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/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts index d8eb969b99b3b..bda336e73c4f8 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 e44a4cb846f2c..5fdf54b98cda6 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 0000000000000..bac87f34ceb82 --- /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/page_objects/endpoint_page.ts b/x-pack/test/functional/page_objects/endpoint_page.ts index f02a899f6d37d..a306a855a83eb 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'); + }, }; } From d27fc476dd53afdf5ad9393e903c4d25de40b257 Mon Sep 17 00:00:00 2001 From: Nathan Reese <reese.nathan@gmail.com> Date: Fri, 14 Feb 2020 08:36:50 -0700 Subject: [PATCH 24/27] [Maps] refactor Join component to remove componentDidUpdate (#57518) * [Maps] refactor Join component to remove componentDidUpdate * [Maps] refactor LayerPanel to remove getDerivedStateFromProps * consolidate async layer state loading into LayerPanel component * fix jest snapshot Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> --- .../__snapshots__/view.test.js.snap | 5 +- .../connected_components/layer_panel/index.js | 4 +- .../layer_panel/join_editor/resources/join.js | 71 +++---------------- .../layer_panel/join_editor/view.js | 4 +- .../connected_components/layer_panel/view.js | 66 +++++++++-------- .../maps/public/layers/vector_layer.js | 6 +- 6 files changed, 56 insertions(+), 100 deletions(-) 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 101716d297b81..7997cde97d89c 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 89ab7cf927d5b..1340d4fd4f219 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 8660fa6010f8a..c2c9f333a675c 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 9f3461e45dfd4..92e32885d43a8 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 50c0949cf91ae..755d4bb6b323a 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 31c3831fb612a..1698d52ea4406 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, From 52b4fe74049655fd1d58b6d108708c549b6f63be Mon Sep 17 00:00:00 2001 From: James Gowdy <jgowdy@elastic.co> Date: Fri, 14 Feb 2020 15:54:49 +0000 Subject: [PATCH 25/27] [ML] Categorization wizard functional tests (#57600) * [ML] Categorization wizard functional tests * changes based on review * some idiot left his own name in the code --- .../detector_cards.tsx | 2 + .../examples_valid_callout.tsx | 6 +- .../categorization_view/field_examples.tsx | 8 +- .../anomaly_detection/categorization_job.ts | 395 ++++++++++++++++++ .../anomaly_detection/index.ts | 1 + .../services/machine_learning/index.ts | 1 + .../machine_learning/job_type_selection.ts | 9 + .../job_wizard_categorization.ts | 63 +++ .../machine_learning/job_wizard_common.ts | 10 + x-pack/test/functional/services/ml.ts | 3 + 10 files changed, 496 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts create mode 100644 x-pack/test/functional/services/machine_learning/job_wizard_categorization.ts 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 68d5fc24a96e3..991e1d5c49b7a 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 ac886a3aea61a..1265063b8aa81 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 51cea179a6c0d..d3f1f0e58698b 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/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 0000000000000..6ea9e694d2955 --- /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 a52e3d3aca2c0..28e8b221cff4e 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/services/machine_learning/index.ts b/x-pack/test/functional/services/machine_learning/index.ts index b01f127519670..4cecd27631e18 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 6686b5b28f200..be66c53326a23 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 0000000000000..cb590c7022965 --- /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 c2f408276d9e4..38e6694669c1a 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 5957a8a2eeb0a..18574c62b84d9 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, From 98564f857d58828ba6de0c3fab80b5cef31cfe1d Mon Sep 17 00:00:00 2001 From: Corey Robertson <corey.robertson@elastic.co> Date: Fri, 14 Feb 2020 10:57:06 -0500 Subject: [PATCH 26/27] Update Canvas usage of Storage from kibana-utils (#55595) Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> --- x-pack/legacy/plugins/canvas/public/legacy.ts | 2 -- .../canvas/public/lib/__tests__/clipboard.js | 16 --------- .../plugins/canvas/public/lib/clipboard.js | 17 --------- .../canvas/public/lib/clipboard.test.ts | 36 +++++++++++++++++++ .../plugins/canvas/public/lib/clipboard.ts | 25 +++++++++++++ .../plugins/canvas/public/lib/get_window.ts | 6 ++-- .../legacy/plugins/canvas/public/plugin.tsx | 3 -- 7 files changed, 65 insertions(+), 40 deletions(-) delete mode 100644 x-pack/legacy/plugins/canvas/public/lib/__tests__/clipboard.js delete mode 100644 x-pack/legacy/plugins/canvas/public/lib/clipboard.js create mode 100644 x-pack/legacy/plugins/canvas/public/lib/clipboard.test.ts create mode 100644 x-pack/legacy/plugins/canvas/public/lib/clipboard.ts diff --git a/x-pack/legacy/plugins/canvas/public/legacy.ts b/x-pack/legacy/plugins/canvas/public/legacy.ts index ea873e6f2296d..0d2e77637f19d 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 c616a76d0dcc3..0000000000000 --- 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 1fd14f086c949..0000000000000 --- 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 0000000000000..54c3000dae36c --- /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 0000000000000..50c5cdd0042fd --- /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 1ee7e68be8485..42c632f4a514f 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 44731628cf653..a5fbbccb4299f 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( From 356e3a47768701740ae99863f8e0089258874a20 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Fri, 14 Feb 2020 08:24:27 -0800 Subject: [PATCH 27/27] [DOCS] Adds Save to Advanced Settings doc (#57696) * [DOCS] Adds Save to Advanced Settings doc * [DOCS] Incorporates review comments --- docs/management/advanced-options.asciidoc | 29 ++++++++++++----------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 9caa3900fccfd..ec626677d0902 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 <<xpack-dashboard-only-mode, dashboard only mode>>. [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.