diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index f2a58e7b6a7ac..c474998e6fd3d 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -3,7 +3,7 @@ library 'kibana-pipeline-library' kibanaLibrary.load() // load from the Jenkins instance -kibanaPipeline(timeoutMinutes: 180) { +kibanaPipeline(timeoutMinutes: 240) { catchErrors { withEnv([ 'CODE_COVERAGE=1', // Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc. diff --git a/.eslintignore b/.eslintignore index 2eaa498f86e5a..2ed9ecf971ff3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,45 +1,48 @@ -node_modules -bower_components -/data -/optimize -/build -/target +**/*.js.snap +**/graphql/types.ts /.es -/plugins +/build /built_assets +/data /html_docs -/src/plugins/data/common/es_query/kuery/ast/_generated_/** -/src/plugins/vis_type_timelion/public/_generated_/** -/src/legacy/ui/public/flot-charts +/optimize +/plugins /test/fixtures/scenarios -/src/legacy/core_plugins/console/public/webpackShims +/x-pack/build +node_modules +target + +!/.eslintrc.js + +# plugin overrides +/src/core/lib/kbn_internal_native_observable /src/legacy/core_plugins/console/public/tests/webpackShims +/src/legacy/core_plugins/console/public/webpackShims +/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken +/src/legacy/ui/public/flot-charts /src/legacy/ui/public/utils/decode_geo_hash.js +/src/plugins/data/common/es_query/kuery/ast/_generated_/** +/src/plugins/vis_type_timelion/public/_generated_/** /src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.* -/src/core/lib/kbn_internal_native_observable -/packages/*/target -/packages/eslint-config-kibana -/packages/kbn-pm/dist -/packages/kbn-plugin-generator/sao_template/template -/packages/kbn-ui-framework/dist -/packages/kbn-ui-framework/doc_site/build -/packages/kbn-ui-framework/generator-kui/*/templates/ -/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/ -/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/ -/x-pack/legacy/plugins/maps/public/vendor/** -/x-pack/coverage -/x-pack/build /x-pack/legacy/plugins/**/__tests__/fixtures/** -/packages/kbn-interpreter/src/common/lib/grammar.js +/x-pack/legacy/plugins/apm/e2e/cypress/**/snapshots.js /x-pack/legacy/plugins/canvas/canvas_plugin +/x-pack/legacy/plugins/canvas/canvas_plugin_src/lib/flot-charts /x-pack/legacy/plugins/canvas/shareable_runtime/build /x-pack/legacy/plugins/canvas/storybook -/x-pack/legacy/plugins/canvas/canvas_plugin_src/lib/flot-charts /x-pack/legacy/plugins/infra/common/graphql/types.ts /x-pack/legacy/plugins/infra/public/graphql/types.ts /x-pack/legacy/plugins/infra/server/graphql/types.ts -/x-pack/legacy/plugins/apm/e2e/cypress/**/snapshots.js -/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken -**/graphql/types.ts -**/*.js.snap -!/.eslintrc.js +/x-pack/legacy/plugins/maps/public/vendor/** + +# package overrides +/packages/eslint-config-kibana +/packages/kbn-interpreter/src/common/lib/grammar.js +/packages/kbn-plugin-generator/sao_template/template +/packages/kbn-pm/dist +/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/ +/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/ +/packages/kbn-ui-framework/dist +/packages/kbn-ui-framework/doc_site/build +/packages/kbn-ui-framework/generator-kui/*/templates/ + diff --git a/.eslintrc.js b/.eslintrc.js index e45a2a96f29d7..c9b41ec711b7f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -742,6 +742,101 @@ module.exports = { }, }, + /** + * Lists overrides + */ + { + // typescript and javascript for front and back end + files: ['x-pack/plugins/lists/**/*.{js,ts,tsx}'], + plugins: ['eslint-plugin-node'], + env: { + mocha: true, + jest: true, + }, + rules: { + 'accessor-pairs': 'error', + 'array-callback-return': 'error', + 'no-array-constructor': 'error', + complexity: 'error', + 'consistent-return': 'error', + 'func-style': ['error', 'expression'], + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], + 'newlines-between': 'always', + }, + ], + 'sort-imports': [ + 'error', + { + ignoreDeclarationSort: true, + }, + ], + 'node/no-deprecated-api': 'error', + 'no-bitwise': 'error', + 'no-continue': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + 'no-duplicate-imports': 'error', + 'no-empty-character-class': 'error', + 'no-empty-pattern': 'error', + 'no-ex-assign': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-extra-boolean-cast': 'error', + 'no-extra-label': 'error', + 'no-func-assign': 'error', + 'no-implicit-globals': 'error', + 'no-implied-eval': 'error', + 'no-invalid-regexp': 'error', + 'no-inner-declarations': 'error', + 'no-lone-blocks': 'error', + 'no-multi-assign': 'error', + 'no-misleading-character-class': 'error', + 'no-new-symbol': 'error', + 'no-obj-calls': 'error', + 'no-param-reassign': ['error', { props: true }], + 'no-process-exit': 'error', + 'no-prototype-builtins': 'error', + 'no-return-await': 'error', + 'no-self-compare': 'error', + 'no-shadow-restricted-names': 'error', + 'no-sparse-arrays': 'error', + 'no-this-before-super': 'error', + 'no-undef': 'error', + 'no-unreachable': 'error', + 'no-unsafe-finally': 'error', + 'no-useless-call': 'error', + 'no-useless-catch': 'error', + 'no-useless-concat': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-escape': 'error', + 'no-useless-rename': 'error', + 'no-useless-return': 'error', + 'no-void': 'error', + 'one-var-declaration-per-line': 'error', + 'prefer-object-spread': 'error', + 'prefer-promise-reject-errors': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'error', + 'require-atomic-updates': 'error', + 'symbol-description': 'error', + 'vars-on-top': 'error', + '@typescript-eslint/explicit-member-accessibility': 'error', + '@typescript-eslint/no-this-alias': 'error', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/unified-signatures': 'error', + '@typescript-eslint/explicit-function-return-type': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/no-unused-vars': 'error', + 'no-template-curly-in-string': 'error', + 'sort-keys': 'error', + 'prefer-destructuring': 'error', + }, + }, /** * Alerting Services overrides */ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4a16952f82014..a97400ee09c0e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -84,7 +84,6 @@ /x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management /x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest-management /x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui -/x-pack/legacy/plugins/uptime @elastic/uptime /x-pack/plugins/uptime @elastic/uptime # Machine Learning @@ -167,8 +166,6 @@ /x-pack/plugins/telemetry_collection_xpack/ @elastic/pulse # Kibana Alerting Services -/x-pack/legacy/plugins/alerting/ @elastic/kibana-alerting-services -/x-pack/legacy/plugins/actions/ @elastic/kibana-alerting-services /x-pack/plugins/alerting/ @elastic/kibana-alerting-services /x-pack/plugins/actions/ @elastic/kibana-alerting-services /x-pack/plugins/event_log/ @elastic/kibana-alerting-services @@ -176,7 +173,6 @@ /x-pack/test/alerting_api_integration/ @elastic/kibana-alerting-services /x-pack/test/plugin_api_integration/plugins/task_manager/ @elastic/kibana-alerting-services /x-pack/test/plugin_api_integration/test_suites/task_manager/ @elastic/kibana-alerting-services -/x-pack/legacy/plugins/triggers_actions_ui/ @elastic/kibana-alerting-services /x-pack/plugins/triggers_actions_ui/ @elastic/kibana-alerting-services /x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/ @elastic/kibana-alerting-services /x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/ @elastic/kibana-alerting-services @@ -209,22 +205,21 @@ /x-pack/plugins/watcher/ @elastic/es-ui # Endpoint -/x-pack/plugins/endpoint/ @elastic/endpoint-app-team -/x-pack/test/api_integration/apis/endpoint/ @elastic/endpoint-app-team -/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team -/x-pack/test/functional_endpoint/ @elastic/endpoint-app-team -/x-pack/test/functional_endpoint_ingest_failure/ @elastic/endpoint-app-team -/x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team -/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team -/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team +/x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/api_integration/apis/endpoint/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/functional_endpoint/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/functional_endpoint_ingest_failure/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team @elastic/siem # SIEM -/x-pack/legacy/plugins/siem/ @elastic/siem -/x-pack/plugins/siem/ @elastic/siem -/x-pack/test/detection_engine_api_integration @elastic/siem -/x-pack/test/api_integration/apis/siem @elastic/siem -/x-pack/plugins/case @elastic/siem +/x-pack/plugins/siem/ @elastic/siem @elastic/endpoint-app-team +/x-pack/test/detection_engine_api_integration @elastic/siem @elastic/endpoint-app-team +/x-pack/test/api_integration/apis/siem @elastic/siem @elastic/endpoint-app-team +/x-pack/plugins/case @elastic/siem @elastic/endpoint-app-team +/x-pack/plugins/lists @elastic/siem @elastic/endpoint-app-team # Security Intelligence And Analytics -/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics /x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md b/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md new file mode 100644 index 0000000000000..51492756ef232 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [defaultPath](./kibana-plugin-core-public.appbase.defaultpath.md) + +## AppBase.defaultPath property + +Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the `path` option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. + +Signature: + +```typescript +defaultPath?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.md b/docs/development/core/public/kibana-plugin-core-public.appbase.md index b73785647f23c..7b624f12ac1df 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.md +++ b/docs/development/core/public/kibana-plugin-core-public.appbase.md @@ -18,6 +18,7 @@ export interface AppBase | [capabilities](./kibana-plugin-core-public.appbase.capabilities.md) | Partial<Capabilities> | Custom capabilities defined by the app. | | [category](./kibana-plugin-core-public.appbase.category.md) | AppCategory | The category definition of the product See [AppCategory](./kibana-plugin-core-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference | | [chromeless](./kibana-plugin-core-public.appbase.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | +| [defaultPath](./kibana-plugin-core-public.appbase.defaultpath.md) | string | Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the path option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. | | [euiIconType](./kibana-plugin-core-public.appbase.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | | [icon](./kibana-plugin-core-public.appbase.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | | [id](./kibana-plugin-core-public.appbase.id.md) | string | The unique identifier of the application | diff --git a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md index cdf9171a46aed..3d8b5d115c8a2 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md +++ b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md @@ -9,5 +9,5 @@ Defines the list of fields that can be updated via an [AppUpdater](./kibana-plug Signature: ```typescript -export declare type AppUpdatableFields = Pick; +export declare type AppUpdatableFields = Pick; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md index 1cc1a1194a537..a9fabb38df869 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md @@ -29,5 +29,5 @@ export interface ChromeNavLink | [subUrlBase](./kibana-plugin-core-public.chromenavlink.suburlbase.md) | string | A url base that legacy apps can set to match deep URLs to an application. | | [title](./kibana-plugin-core-public.chromenavlink.title.md) | string | The title of the application. | | [tooltip](./kibana-plugin-core-public.chromenavlink.tooltip.md) | string | A tooltip shown when hovering over an app link. | -| [url](./kibana-plugin-core-public.chromenavlink.url.md) | string | A url that legacy apps can set to deep link into their applications. | +| [url](./kibana-plugin-core-public.chromenavlink.url.md) | string | The route used to open the [default path](./kibana-plugin-core-public.appbase.defaultpath.md) of an application. If unset, baseUrl will be used instead. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md index 0c415ed1a7fad..1e0b890015993 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md @@ -4,11 +4,7 @@ ## ChromeNavLink.url property -> Warning: This API is now obsolete. -> -> - -A url that legacy apps can set to deep link into their applications. +The route used to open the [default path](./kibana-plugin-core-public.appbase.defaultpath.md) of an application. If unset, `baseUrl` will be used instead. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md index a502c40db0cd8..a3294fb0a087a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md @@ -9,22 +9,36 @@ A migration function for a [saved object type](./kibana-plugin-core-server.saved Signature: ```typescript -export declare type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; +export declare type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; ``` ## Example ```typescript -const migrateProperty: SavedObjectMigrationFn = (doc, { log }) => { - if(doc.attributes.someProp === null) { - log.warn('Skipping migration'); - } else { - doc.attributes.someProp = migrateProperty(doc.attributes.someProp); - } - - return doc; +interface TypeV1Attributes { + someKey: string; + obsoleteProperty: number; } +interface TypeV2Attributes { + someKey: string; + newProperty: string; +} + +const migrateToV2: SavedObjectMigrationFn = (doc, { log }) => { + const { obsoleteProperty, ...otherAttributes } = doc.attributes; + // instead of mutating `doc` we make a shallow copy so that we can use separate types for the input + // and output attributes. We don't need to make a deep copy, we just need to ensure that obsolete + // attributes are not present on the returned doc. + return { + ...doc, + attributes: { + ...otherAttributes, + newProperty: migrate(obsoleteProperty), + }, + }; +}; + ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md index 6d4e252fe7532..3f4090619edbf 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md @@ -9,5 +9,5 @@ Describes Saved Object documents that have passed through the migration framewor Signature: ```typescript -export declare type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; +export declare type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md index be51400addbbc..8e2395ee6310d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md @@ -9,5 +9,5 @@ Describes Saved Object documents from Kibana < 7.0.0 which don't have a `refe Signature: ```typescript -export declare type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; +export declare type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md index 04a0d871cab2d..3969a97fa7789 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md @@ -7,7 +7,10 @@ Signature: ```typescript -export declare function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, forceNow?: Date): import("../..").RangeFilter | undefined; +export declare function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, options?: { + forceNow?: Date; + fieldName?: string; +}): import("../..").RangeFilter | undefined; ``` ## Parameters @@ -16,7 +19,7 @@ export declare function getTime(indexPattern: IIndexPattern | undefined, timeRan | --- | --- | --- | | indexPattern | IIndexPattern | undefined | | | timeRange | TimeRange | | -| forceNow | Date | | +| options | {
forceNow?: Date;
fieldName?: string;
} | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md new file mode 100644 index 0000000000000..c3998876c9712 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) > [getTimeField](./kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md) + +## IIndexPattern.getTimeField() method + +Signature: + +```typescript +getTimeField?(): IFieldType | undefined; +``` +Returns: + +`IFieldType | undefined` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md index 1bbd6cf67f0ce..1cb89822eb605 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md @@ -21,3 +21,9 @@ export interface IIndexPattern | [title](./kibana-plugin-plugins-data-public.iindexpattern.title.md) | string | | | [type](./kibana-plugin-plugins-data-public.iindexpattern.type.md) | string | | +## Methods + +| Method | Description | +| --- | --- | +| [getTimeField()](./kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md) | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 0fd82ffb2240c..e1df493143b73 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -43,7 +43,7 @@ | [getEsPreference(uiSettings, sessionId)](./kibana-plugin-plugins-data-public.getespreference.md) | | | [getQueryLog(uiSettings, storage, appName, language)](./kibana-plugin-plugins-data-public.getquerylog.md) | | | [getSearchErrorType({ message })](./kibana-plugin-plugins-data-public.getsearcherrortype.md) | | -| [getTime(indexPattern, timeRange, forceNow)](./kibana-plugin-plugins-data-public.gettime.md) | | +| [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-public.gettime.md) | | | [plugin(initializerContext)](./kibana-plugin-plugins-data-public.plugin.md) | | ## Interfaces diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 7411e2df1b613..cc3fa8c2720de 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -37,7 +37,7 @@ const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset' const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); const STATIC_BUNDLE_PLUGINS = [ - // { id: 'data', dirname: 'data' }, + { id: 'data', dirname: 'data' }, { id: 'kibanaReact', dirname: 'kibana_react' }, { id: 'kibanaUtils', dirname: 'kibana_utils' }, { id: 'esUiShared', dirname: 'es_ui_shared' }, @@ -60,13 +60,8 @@ function dynamicExternals(bundle: Bundle, context: string, request: string) { return; } - // don't allow any static bundle to rely on other static bundles - if (STATIC_BUNDLE_PLUGINS.some(p => bundle.id === p.id)) { - return; - } - - // ignore requests that don't include a /data/public, /kibana_react/public, or - // /kibana_utils/public segment as a cheap way to avoid doing path resolution + // ignore requests that don't include a /{dirname}/public for one of our + // "static" bundles as a cheap way to avoid doing path resolution // for paths that couldn't possibly resolve to what we're looking for const reqToStaticBundle = STATIC_BUNDLE_PLUGINS.some(p => request.includes(`/${p.dirname}/public`) diff --git a/renovate.json5 b/renovate.json5 index ffa006264873d..c0ddcaf4f23c8 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -846,6 +846,14 @@ '@types/semver', ], }, + { + groupSlug: 'set-value', + groupName: 'set-value related packages', + packageNames: [ + 'set-value', + '@types/set-value', + ], + }, { groupSlug: 'sinon', groupName: 'sinon related packages', diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 8c5fe4875aaea..c91c00bc1aa02 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -957,7 +957,7 @@ const migration = (doc, log) => {...} Would be converted to: ```typescript -const migration: SavedObjectMigrationFn = (doc, { log }) => {...} +const migration: SavedObjectMigrationFn = (doc, { log }) => {...} ``` ### Remarks diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index c25918c6b7328..e29837aecb125 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -87,7 +87,7 @@ describe('#setup()', () => { ).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`); }); - it('allows to register a statusUpdater for the application', async () => { + it('allows to register an AppUpdater for the application', async () => { const setup = service.setup(setupDeps); const pluginId = Symbol('plugin'); @@ -118,6 +118,7 @@ describe('#setup()', () => { updater$.next(app => ({ status: AppStatus.inaccessible, tooltip: 'App inaccessible due to reason', + defaultPath: 'foo/bar', })); applications = await applications$.pipe(take(1)).toPromise(); @@ -128,6 +129,7 @@ describe('#setup()', () => { legacy: false, navLinkStatus: AppNavLinkStatus.default, status: AppStatus.inaccessible, + defaultPath: 'foo/bar', tooltip: 'App inaccessible due to reason', }) ); @@ -209,7 +211,7 @@ describe('#setup()', () => { }); }); - describe('registerAppStatusUpdater', () => { + describe('registerAppUpdater', () => { it('updates status fields', async () => { const setup = service.setup(setupDeps); @@ -413,6 +415,36 @@ describe('#setup()', () => { }) ); }); + + it('allows to update the basePath', async () => { + const setup = service.setup(setupDeps); + + const pluginId = Symbol('plugin'); + setup.register(pluginId, createApp({ id: 'app1' })); + + const updater = new BehaviorSubject(app => ({})); + setup.registerAppUpdater(updater); + + const start = await service.start(startDeps); + await start.navigateToApp('app1'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1', undefined); + MockHistory.push.mockClear(); + + updater.next(app => ({ defaultPath: 'default-path' })); + await start.navigateToApp('app1'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/default-path', undefined); + MockHistory.push.mockClear(); + + updater.next(app => ({ defaultPath: 'another-path' })); + await start.navigateToApp('app1'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/another-path', undefined); + MockHistory.push.mockClear(); + + updater.next(app => ({})); + await start.navigateToApp('app1'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1', undefined); + MockHistory.push.mockClear(); + }); }); it("`registerMountContext` calls context container's registerContext", () => { @@ -676,6 +708,57 @@ describe('#start()', () => { expect(MockHistory.push).toHaveBeenCalledWith('/custom/path#/hash/router/path', undefined); }); + it('preserves trailing slash when path contains a hash', async () => { + const { register } = service.setup(setupDeps); + + register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/app-path' })); + + const { navigateToApp } = await service.start(startDeps); + await navigateToApp('app2', { path: '#/' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path#/', undefined); + MockHistory.push.mockClear(); + + await navigateToApp('app2', { path: '#/foo/bar/' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path#/foo/bar/', undefined); + MockHistory.push.mockClear(); + + await navigateToApp('app2', { path: '/path#/' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path/path#/', undefined); + MockHistory.push.mockClear(); + + await navigateToApp('app2', { path: '/path#/hash/' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path/path#/hash/', undefined); + MockHistory.push.mockClear(); + + await navigateToApp('app2', { path: '/path/' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path/path', undefined); + MockHistory.push.mockClear(); + }); + + it('appends the defaultPath when the path parameter is not specified', async () => { + const { register } = service.setup(setupDeps); + + register(Symbol(), createApp({ id: 'app1', defaultPath: 'default/path' })); + register( + Symbol(), + createApp({ id: 'app2', appRoute: '/custom-app-path', defaultPath: '/my-base' }) + ); + + const { navigateToApp } = await service.start(startDeps); + + await navigateToApp('app1', { path: 'defined-path' }); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/defined-path', undefined); + + await navigateToApp('app1', {}); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/default/path', undefined); + + await navigateToApp('app2', { path: 'defined-path' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom-app-path/defined-path', undefined); + + await navigateToApp('app2', {}); + expect(MockHistory.push).toHaveBeenCalledWith('/custom-app-path/my-base', undefined); + }); + it('includes state if specified', async () => { const { register } = service.setup(setupDeps); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 1c9492d81c7f6..bafa1932e5e92 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -46,6 +46,7 @@ import { Mounter, } from './types'; import { getLeaveAction, isConfirmAction } from './application_leave'; +import { appendAppPath } from './utils'; interface SetupDeps { context: ContextSetup; @@ -81,13 +82,7 @@ const getAppUrl = (mounters: Map, appId: string, path: string = const appBasePath = mounters.get(appId)?.appRoute ? `/${mounters.get(appId)!.appRoute}` : `/app/${appId}`; - - // Only preppend slash if not a hash or query path - path = path.startsWith('#') || path.startsWith('?') ? path : `/${path}`; - - return `${appBasePath}${path}` - .replace(/\/{2,}/g, '/') // Remove duplicate slashes - .replace(/\/$/, ''); // Remove trailing slash + return appendAppPath(appBasePath, path); }; const allApplicationsFilter = '__ALL__'; @@ -290,6 +285,9 @@ export class ApplicationService { }, navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => { if (await this.shouldNavigate(overlays)) { + if (path === undefined) { + path = applications$.value.get(appId)?.defaultPath; + } this.appLeaveHandlers.delete(this.currentAppId$.value!); this.navigate!(getAppUrl(availableMounters, appId, path), state); this.currentAppId$.next(appId); diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 318afb652999e..0734e178033e2 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -66,6 +66,13 @@ export interface AppBase { */ navLinkStatus?: AppNavLinkStatus; + /** + * Allow to define the default path a user should be directed to when navigating to the app. + * When defined, this value will be used as a default for the `path` option when calling {@link ApplicationStart.navigateToApp | navigateToApp}`, + * and will also be appended to the {@link ChromeNavLink | application navLink} in the navigation bar. + */ + defaultPath?: string; + /** * An {@link AppUpdater} observable that can be used to update the application {@link AppUpdatableFields} at runtime. * @@ -187,7 +194,10 @@ export enum AppNavLinkStatus { * Defines the list of fields that can be updated via an {@link AppUpdater}. * @public */ -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick< + AppBase, + 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' +>; /** * Updater for applications. @@ -642,7 +652,8 @@ export interface ApplicationStart { * Navigate to a given app * * @param appId - * @param options.path - optional path inside application to deep link to + * @param options.path - optional path inside application to deep link to. + * If undefined, will use {@link AppBase.defaultPath | the app's default path}` as default. * @param options.state - optional state to forward to the application */ navigateToApp(appId: string, options?: { path?: string; state?: any }): Promise; diff --git a/src/core/public/application/utils.test.ts b/src/core/public/application/utils.test.ts new file mode 100644 index 0000000000000..7ed0919f88c61 --- /dev/null +++ b/src/core/public/application/utils.test.ts @@ -0,0 +1,71 @@ +/* + * 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 { removeSlashes, appendAppPath } from './utils'; + +describe('removeSlashes', () => { + it('only removes duplicates by default', () => { + expect(removeSlashes('/some//url//to//')).toEqual('/some/url/to/'); + expect(removeSlashes('some/////other//url')).toEqual('some/other/url'); + }); + + it('remove trailing slash when `trailing` is true', () => { + expect(removeSlashes('/some//url//to//', { trailing: true })).toEqual('/some/url/to'); + }); + + it('remove leading slash when `leading` is true', () => { + expect(removeSlashes('/some//url//to//', { leading: true })).toEqual('some/url/to/'); + }); + + it('does not removes duplicates when `duplicates` is false', () => { + expect(removeSlashes('/some//url//to/', { leading: true, duplicates: false })).toEqual( + 'some//url//to/' + ); + expect(removeSlashes('/some//url//to/', { trailing: true, duplicates: false })).toEqual( + '/some//url//to' + ); + }); + + it('accept mixed options', () => { + expect( + removeSlashes('/some//url//to/', { leading: true, duplicates: false, trailing: true }) + ).toEqual('some//url//to'); + expect( + removeSlashes('/some//url//to/', { leading: true, duplicates: true, trailing: true }) + ).toEqual('some/url/to'); + }); +}); + +describe('appendAppPath', () => { + it('appends the appBasePath with given path', () => { + expect(appendAppPath('/app/my-app', '/some-path')).toEqual('/app/my-app/some-path'); + expect(appendAppPath('/app/my-app/', 'some-path')).toEqual('/app/my-app/some-path'); + expect(appendAppPath('/app/my-app', 'some-path')).toEqual('/app/my-app/some-path'); + expect(appendAppPath('/app/my-app', '')).toEqual('/app/my-app'); + }); + + it('preserves the trailing slash only if included in the hash', () => { + expect(appendAppPath('/app/my-app', '/some-path/')).toEqual('/app/my-app/some-path'); + expect(appendAppPath('/app/my-app', '/some-path#/')).toEqual('/app/my-app/some-path#/'); + expect(appendAppPath('/app/my-app', '/some-path#/hash/')).toEqual( + '/app/my-app/some-path#/hash/' + ); + expect(appendAppPath('/app/my-app', '/some-path#/hash')).toEqual('/app/my-app/some-path#/hash'); + }); +}); diff --git a/src/core/public/application/utils.ts b/src/core/public/application/utils.ts new file mode 100644 index 0000000000000..048f195fe1223 --- /dev/null +++ b/src/core/public/application/utils.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Utility to remove trailing, leading or duplicate slashes. + * By default will only remove duplicates. + */ +export const removeSlashes = ( + url: string, + { + trailing = false, + leading = false, + duplicates = true, + }: { trailing?: boolean; leading?: boolean; duplicates?: boolean } = {} +): string => { + if (duplicates) { + url = url.replace(/\/{2,}/g, '/'); + } + if (trailing) { + url = url.replace(/\/$/, ''); + } + if (leading) { + url = url.replace(/^\//, ''); + } + return url; +}; + +export const appendAppPath = (appBasePath: string, path: string = '') => { + // Only prepend slash if not a hash or query path + path = path === '' || path.startsWith('#') || path.startsWith('?') ? path : `/${path}`; + // Do not remove trailing slash when in hashbang + const removeTrailing = path.indexOf('#') === -1; + return removeSlashes(`${appBasePath}${path}`, { + trailing: removeTrailing, + duplicates: true, + leading: false, + }); +}; diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index d0ef2aeb265fe..fb2972735c2b7 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -44,6 +44,12 @@ export interface ChromeNavLink { */ readonly baseUrl: string; + /** + * The route used to open the {@link AppBase.defaultPath | default path } of an application. + * If unset, `baseUrl` will be used instead. + */ + readonly url?: string; + /** * An ordinal used to sort nav links relative to one another for display. */ @@ -99,18 +105,6 @@ export interface ChromeNavLink { */ readonly linkToLastSubUrl?: boolean; - /** - * A url that legacy apps can set to deep link into their applications. - * - * @internalRemarks - * Currently used by the "lastSubUrl" feature legacy/ui/chrome. This should - * be removed once the ApplicationService is implemented and mounting apps. At that - * time, each app can handle opening to the previous location when they are mounted. - * - * @deprecated - */ - readonly url?: string; - /** * Indicates whether or not this app is currently on the screen. * diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts index 23fdabe0f3430..4c319873af804 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.test.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts @@ -85,6 +85,38 @@ describe('toNavLink', () => { expect(link.properties.baseUrl).toEqual('http://localhost/base-path/my-route/my-path'); }); + it('generates the `url` property', () => { + let link = toNavLink( + app({ + appRoute: '/my-route/my-path', + }), + basePath + ); + expect(link.properties.url).toEqual('http://localhost/base-path/my-route/my-path'); + + link = toNavLink( + app({ + appRoute: '/my-route/my-path', + defaultPath: 'some/default/path', + }), + basePath + ); + expect(link.properties.url).toEqual( + 'http://localhost/base-path/my-route/my-path/some/default/path' + ); + }); + + it('does not generate `url` for legacy app', () => { + const link = toNavLink( + legacyApp({ + appUrl: '/my-legacy-app/#foo', + defaultPath: '/some/default/path', + }), + basePath + ); + expect(link.properties.url).toBeUndefined(); + }); + it('uses appUrl when converting legacy applications', () => { expect( toNavLink( diff --git a/src/core/public/chrome/nav_links/to_nav_link.ts b/src/core/public/chrome/nav_links/to_nav_link.ts index 18e4b7b26b6ba..f79b1df77f8e1 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.ts @@ -20,9 +20,11 @@ import { App, AppNavLinkStatus, AppStatus, LegacyApp } from '../../application'; import { IBasePath } from '../../http'; import { NavLinkWrapper } from './nav_link'; +import { appendAppPath } from '../../application/utils'; export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWrapper { const useAppStatus = app.navLinkStatus === AppNavLinkStatus.default; + const baseUrl = isLegacyApp(app) ? basePath.prepend(app.appUrl) : basePath.prepend(app.appRoute!); return new NavLinkWrapper({ ...app, hidden: useAppStatus @@ -30,9 +32,12 @@ export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWra : app.navLinkStatus === AppNavLinkStatus.hidden, disabled: useAppStatus ? false : app.navLinkStatus === AppNavLinkStatus.disabled, legacy: isLegacyApp(app), - baseUrl: isLegacyApp(app) - ? relativeToAbsolute(basePath.prepend(app.appUrl)) - : relativeToAbsolute(basePath.prepend(app.appRoute!)), + baseUrl: relativeToAbsolute(baseUrl), + ...(isLegacyApp(app) + ? {} + : { + url: relativeToAbsolute(appendAppPath(baseUrl, app.defaultPath)), + }), }); } diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index 52b59c53b658c..d97ef477c2ee0 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -53,7 +53,7 @@ export function euiNavLink( order, tooltip, } = navLink; - let href = navLink.baseUrl; + let href = navLink.url ?? navLink.baseUrl; if (legacy) { href = url && !active ? url : baseUrl; diff --git a/src/core/public/plugins/plugin.test.mocks.ts b/src/core/public/plugins/plugin.test.mocks.ts index b877847aaa90e..422442c9ca4d2 100644 --- a/src/core/public/plugins/plugin.test.mocks.ts +++ b/src/core/public/plugins/plugin.test.mocks.ts @@ -24,8 +24,8 @@ export const mockPlugin = { }; export const mockInitializer = jest.fn(() => mockPlugin); -export const mockPluginLoader = jest.fn().mockResolvedValue(mockInitializer); +export const mockPluginReader = jest.fn(() => mockInitializer); -jest.mock('./plugin_loader', () => ({ - loadPluginBundle: mockPluginLoader, +jest.mock('./plugin_reader', () => ({ + read: mockPluginReader, })); diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts index 39330711f7980..8fe745db9554d 100644 --- a/src/core/public/plugins/plugin.test.ts +++ b/src/core/public/plugins/plugin.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { mockInitializer, mockPlugin, mockPluginLoader } from './plugin.test.mocks'; +import { mockInitializer, mockPlugin, mockPluginReader } from './plugin.test.mocks'; import { DiscoveredPlugin } from '../../server'; import { coreMock } from '../mocks'; @@ -38,10 +38,9 @@ function createManifest( let plugin: PluginWrapper>; const opaqueId = Symbol(); const initializerContext = coreMock.createPluginInitializerContext(); -const addBasePath = (path: string) => path; beforeEach(() => { - mockPluginLoader.mockClear(); + mockPluginReader.mockClear(); mockPlugin.setup.mockClear(); mockPlugin.start.mockClear(); mockPlugin.stop.mockClear(); @@ -49,20 +48,8 @@ beforeEach(() => { }); describe('PluginWrapper', () => { - test('`load` calls loadPluginBundle', () => { - plugin.load(addBasePath); - expect(mockPluginLoader).toHaveBeenCalledWith(addBasePath, 'plugin-a'); - }); - - test('`setup` fails if load is not called first', async () => { - await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Plugin \\"plugin-a\\" can't be setup since its bundle isn't loaded."` - ); - }); - test('`setup` fails if plugin.setup is not a function', async () => { mockInitializer.mockReturnValueOnce({ start: jest.fn() } as any); - await plugin.load(addBasePath); await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"setup\\" function."` ); @@ -70,20 +57,17 @@ describe('PluginWrapper', () => { test('`setup` fails if plugin.start is not a function', async () => { mockInitializer.mockReturnValueOnce({ setup: jest.fn() } as any); - await plugin.load(addBasePath); await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"start\\" function."` ); }); test('`setup` calls initializer with initializer context', async () => { - await plugin.load(addBasePath); await plugin.setup({} as any, {} as any); expect(mockInitializer).toHaveBeenCalledWith(initializerContext); }); test('`setup` calls plugin.setup with context and dependencies', async () => { - await plugin.load(addBasePath); const context = { any: 'thing' } as any; const deps = { otherDep: 'value' }; await plugin.setup(context, deps); @@ -91,14 +75,12 @@ describe('PluginWrapper', () => { }); test('`start` fails if setup is not called first', async () => { - await plugin.load(addBasePath); await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( `"Plugin \\"plugin-a\\" can't be started since it isn't set up."` ); }); test('`start` calls plugin.start with context and dependencies', async () => { - await plugin.load(addBasePath); await plugin.setup({} as any, {} as any); const context = { any: 'thing' } as any; const deps = { otherDep: 'value' }; @@ -114,20 +96,21 @@ describe('PluginWrapper', () => { }; let startDependenciesResolved = false; - mockPluginLoader.mockResolvedValueOnce(() => ({ - setup: jest.fn(), - start: async () => { - // Add small delay to ensure startDependencies is not resolved until after the plugin instance's start resolves. - await new Promise(resolve => setTimeout(resolve, 10)); - expect(startDependenciesResolved).toBe(false); - return pluginStartContract; - }, - })); - await plugin.load(addBasePath); + mockPluginReader.mockReturnValueOnce( + jest.fn(() => ({ + setup: jest.fn(), + start: jest.fn(async () => { + // Add small delay to ensure startDependencies is not resolved until after the plugin instance's start resolves. + await new Promise(resolve => setTimeout(resolve, 10)); + expect(startDependenciesResolved).toBe(false); + return pluginStartContract; + }), + stop: jest.fn(), + })) + ); await plugin.setup({} as any, {} as any); const context = { any: 'thing' } as any; const deps = { otherDep: 'value' }; - // Add promise callback prior to calling `start` to ensure calls in `setup` will not resolve before `start` is // called. const startDependenciesCheck = plugin.startDependencies.then(res => { @@ -145,7 +128,6 @@ describe('PluginWrapper', () => { }); test('`stop` calls plugin.stop', async () => { - await plugin.load(addBasePath); await plugin.setup({} as any, {} as any); await plugin.stop(); expect(mockPlugin.stop).toHaveBeenCalled(); @@ -153,7 +135,6 @@ describe('PluginWrapper', () => { test('`stop` does not fail if plugin.stop does not exist', async () => { mockInitializer.mockReturnValueOnce({ setup: jest.fn(), start: jest.fn() } as any); - await plugin.load(addBasePath); await plugin.setup({} as any, {} as any); expect(() => plugin.stop()).not.toThrow(); }); diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index e51c45040c452..591165fcd2839 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -21,7 +21,7 @@ import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; import { DiscoveredPlugin, PluginOpaqueId } from '../../server'; import { PluginInitializerContext } from './plugin_context'; -import { loadPluginBundle } from './plugin_loader'; +import { read } from './plugin_reader'; import { CoreStart, CoreSetup } from '..'; /** @@ -69,7 +69,6 @@ export class PluginWrapper< public readonly configPath: DiscoveredPlugin['configPath']; public readonly requiredPlugins: DiscoveredPlugin['requiredPlugins']; public readonly optionalPlugins: DiscoveredPlugin['optionalPlugins']; - private initializer?: PluginInitializer; private instance?: Plugin; private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>(); @@ -86,18 +85,6 @@ export class PluginWrapper< this.optionalPlugins = discoveredPlugin.optionalPlugins; } - /** - * Loads the plugin's bundle into the browser. Should be called in parallel with all plugins - * using `Promise.all`. Must be called before `setup`. - * @param addBasePath Function that adds the base path to a string for plugin bundle path. - */ - public async load(addBasePath: (path: string) => string) { - this.initializer = await loadPluginBundle( - addBasePath, - this.name - ); - } - /** * Instantiates plugin and calls `setup` function exposed by the plugin initializer. * @param setupContext Context that consists of various core services tailored specifically @@ -146,11 +133,14 @@ export class PluginWrapper< } private async createPluginInstance() { - if (this.initializer === undefined) { - throw new Error(`Plugin "${this.name}" can't be setup since its bundle isn't loaded.`); - } - - const instance = this.initializer(this.initializerContext); + const initializer = read(this.name) as PluginInitializer< + TSetup, + TStart, + TPluginsSetup, + TPluginsStart + >; + + const instance = initializer(this.initializerContext); if (typeof instance.setup !== 'function') { throw new Error(`Instance of plugin "${this.name}" does not define "setup" function.`); diff --git a/src/core/public/plugins/plugin_loader.test.ts b/src/core/public/plugins/plugin_loader.test.ts deleted file mode 100644 index b4e2c3095f14a..0000000000000 --- a/src/core/public/plugins/plugin_loader.test.ts +++ /dev/null @@ -1,125 +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 { CoreWindow, loadPluginBundle } from './plugin_loader'; - -let createdScriptTags = [] as any[]; -let appendChildSpy: jest.SpyInstance; -let createElementSpy: jest.SpyInstance< - HTMLElement, - [string, (ElementCreationOptions | undefined)?] ->; - -const coreWindow = (window as unknown) as CoreWindow; - -beforeEach(() => { - // Mock document.createElement to return fake tags we can use to inspect what - // loadPluginBundles does. - createdScriptTags = []; - createElementSpy = jest.spyOn(document, 'createElement').mockImplementation(() => { - const scriptTag = { setAttribute: jest.fn() } as any; - createdScriptTags.push(scriptTag); - return scriptTag; - }); - - // Mock document.body.appendChild to avoid errors about appending objects that aren't `Node`'s - // and so we can verify that the script tags were added to the page. - appendChildSpy = jest.spyOn(document.body, 'appendChild').mockReturnValue({} as any); - - // Mock global fields needed for loading modules. - coreWindow.__kbnBundles__ = {}; -}); - -afterEach(() => { - appendChildSpy.mockRestore(); - createElementSpy.mockRestore(); - delete coreWindow.__kbnBundles__; -}); - -const addBasePath = (path: string) => path; - -test('`loadPluginBundles` creates a script tag and loads initializer', async () => { - const loadPromise = loadPluginBundle(addBasePath, 'plugin-a'); - - // Verify it sets up the script tag correctly and adds it to document.body - expect(createdScriptTags).toHaveLength(1); - const fakeScriptTag = createdScriptTags[0]; - expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith( - 'src', - '/bundles/plugin/plugin-a/plugin-a.plugin.js' - ); - expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith('id', 'kbn-plugin-plugin-a'); - expect(fakeScriptTag.onload).toBeInstanceOf(Function); - expect(fakeScriptTag.onerror).toBeInstanceOf(Function); - expect(appendChildSpy).toHaveBeenCalledWith(fakeScriptTag); - - // Setup a fake initializer as if a plugin bundle had actually been loaded. - const fakeInitializer = jest.fn(); - coreWindow.__kbnBundles__['plugin/plugin-a'] = { plugin: fakeInitializer }; - // Call the onload callback - fakeScriptTag.onload(); - await expect(loadPromise).resolves.toEqual(fakeInitializer); -}); - -test('`loadPluginBundles` includes the basePath', async () => { - loadPluginBundle((path: string) => `/mybasepath${path}`, 'plugin-a'); - - // Verify it sets up the script tag correctly and adds it to document.body - expect(createdScriptTags).toHaveLength(1); - const fakeScriptTag = createdScriptTags[0]; - expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith( - 'src', - '/mybasepath/bundles/plugin/plugin-a/plugin-a.plugin.js' - ); -}); - -test('`loadPluginBundles` rejects if script.onerror is called', async () => { - const loadPromise = loadPluginBundle(addBasePath, 'plugin-a'); - const fakeScriptTag1 = createdScriptTags[0]; - // Call the error on the second script - fakeScriptTag1.onerror(new Error('Whoa there!')); - - await expect(loadPromise).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to load \\"plugin-a\\" bundle (/bundles/plugin/plugin-a/plugin-a.plugin.js)"` - ); -}); - -test('`loadPluginBundles` rejects if timeout is reached', async () => { - await expect( - // Override the timeout to 1 ms for testi. - loadPluginBundle(addBasePath, 'plugin-a', { timeoutMs: 1 }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Timeout reached when loading \\"plugin-a\\" bundle (/bundles/plugin/plugin-a/plugin-a.plugin.js)"` - ); -}); - -test('`loadPluginBundles` rejects if bundle does attach an initializer to window.__kbnBundles__', async () => { - const loadPromise = loadPluginBundle(addBasePath, 'plugin-a'); - - const fakeScriptTag1 = createdScriptTags[0]; - - // Setup a fake initializer as if a plugin bundle had actually been loaded. - coreWindow.__kbnBundles__['plugin/plugin-a'] = undefined; - // Call the onload callback - fakeScriptTag1.onload(); - - await expect(loadPromise).rejects.toThrowErrorMatchingInlineSnapshot( - `"Definition of plugin \\"plugin-a\\" should be a function (/bundles/plugin/plugin-a/plugin-a.plugin.js)."` - ); -}); diff --git a/src/core/public/plugins/plugin_loader.ts b/src/core/public/plugins/plugin_loader.ts deleted file mode 100644 index bf7711055e97b..0000000000000 --- a/src/core/public/plugins/plugin_loader.ts +++ /dev/null @@ -1,141 +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 { PluginName } from '../../server'; -import { PluginInitializer } from './plugin'; - -/** - * Unknown variant for internal use only for when plugins are not known. - * @internal - */ -export type UnknownPluginInitializer = PluginInitializer>; - -/** - * Custom window type for loading bundles. Do not extend global Window to avoid leaking these types. - * @internal - */ -export interface CoreWindow { - __kbnBundles__: { - [pluginBundleName: string]: { plugin: UnknownPluginInitializer } | undefined; - }; -} - -/** - * Timeout for loading a single script in milliseconds. - * @internal - */ -export const LOAD_TIMEOUT = 120 * 1000; // 2 minutes - -/** - * Loads the bundle for a plugin onto the page and returns their PluginInitializer. This should - * be called for all plugins (once per plugin) in parallel using Promise.all. - * - * If this is slowing down browser load time, there are some ways we could make this faster: - * - Add these bundles in the generated bootstrap.js file so they're loaded immediately - * - Concatenate all the bundles files on the backend and serve them in single request. - * - Use HTTP/2 to load these bundles without having to open new connections for each. - * - * This may not be much of an issue since these should be cached by the browser after the first - * page load. - * - * @param basePath - * @param plugins - * @internal - */ -export const loadPluginBundle: LoadPluginBundle = < - TSetup, - TStart, - TPluginsSetup extends object, - TPluginsStart extends object ->( - addBasePath: (path: string) => string, - pluginName: PluginName, - { timeoutMs = LOAD_TIMEOUT }: { timeoutMs?: number } = {} -) => - new Promise>( - (resolve, reject) => { - const coreWindow = (window as unknown) as CoreWindow; - const exportId = `plugin/${pluginName}`; - - const readPluginExport = () => { - const PluginExport: any = coreWindow.__kbnBundles__[exportId]; - if (typeof PluginExport?.plugin !== 'function') { - reject( - new Error(`Definition of plugin "${pluginName}" should be a function (${bundlePath}).`) - ); - } else { - resolve( - PluginExport.plugin as PluginInitializer - ); - } - }; - - if (coreWindow.__kbnBundles__[exportId]) { - readPluginExport(); - return; - } - - const script = document.createElement('script'); - // Assumes that all plugin bundles get put into the bundles/plugins subdirectory - const bundlePath = addBasePath(`/bundles/plugin/${pluginName}/${pluginName}.plugin.js`); - script.setAttribute('src', bundlePath); - script.setAttribute('id', `kbn-plugin-${pluginName}`); - script.setAttribute('async', ''); - - const cleanupTag = () => { - clearTimeout(timeout); - // Set to null for IE memory leak issue. Webpack does the same thing. - // @ts-ignore - script.onload = script.onerror = null; - }; - - // Wire up resolve and reject - script.onload = () => { - cleanupTag(); - readPluginExport(); - }; - - script.onerror = () => { - cleanupTag(); - reject(new Error(`Failed to load "${pluginName}" bundle (${bundlePath})`)); - }; - - const timeout = setTimeout(() => { - cleanupTag(); - reject(new Error(`Timeout reached when loading "${pluginName}" bundle (${bundlePath})`)); - }, timeoutMs); - - // Add the script tag to the end of the body to start downloading - document.body.appendChild(script); - } - ); - -/** - * @internal - */ -export type LoadPluginBundle = < - TSetup, - TStart, - TPluginsSetup extends object, - TPluginsStart extends object ->( - addBasePath: (path: string) => string, - pluginName: PluginName, - options?: { timeoutMs?: number } -) => Promise>; diff --git a/src/core/public/plugins/plugin_reader.test.ts b/src/core/public/plugins/plugin_reader.test.ts new file mode 100644 index 0000000000000..d4324f81de8e6 --- /dev/null +++ b/src/core/public/plugins/plugin_reader.test.ts @@ -0,0 +1,53 @@ +/* + * 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 { CoreWindow, read, UnknownPluginInitializer } from './plugin_reader'; + +const coreWindow: CoreWindow = window as any; +beforeEach(() => { + coreWindow.__kbnBundles__ = {}; +}); + +it('handles undefined plugin exports', () => { + coreWindow.__kbnBundles__['plugin/foo'] = undefined; + + expect(() => { + read('foo'); + }).toThrowError(`Definition of plugin "foo" not found and may have failed to load.`); +}); + +it('handles plugin exports with a "plugin" export that is not a function', () => { + coreWindow.__kbnBundles__['plugin/foo'] = { + plugin: 1234, + } as any; + + expect(() => { + read('foo'); + }).toThrowError(`Definition of plugin "foo" should be a function.`); +}); + +it('returns the plugin initializer when the "plugin" named export is a function', () => { + const plugin: UnknownPluginInitializer = () => { + return undefined as any; + }; + + coreWindow.__kbnBundles__['plugin/foo'] = { plugin }; + + expect(read('foo')).toBe(plugin); +}); diff --git a/src/core/public/plugins/plugin_reader.ts b/src/core/public/plugins/plugin_reader.ts new file mode 100644 index 0000000000000..1907dfa6a3e99 --- /dev/null +++ b/src/core/public/plugins/plugin_reader.ts @@ -0,0 +1,52 @@ +/* + * 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 { PluginInitializer } from './plugin'; + +/** + * Unknown variant for internal use only for when plugins are not known. + * @internal + */ +export type UnknownPluginInitializer = PluginInitializer>; + +/** + * Custom window type for loading bundles. Do not extend global Window to avoid leaking these types. + * @internal + */ +export interface CoreWindow { + __kbnBundles__: { + [pluginBundleName: string]: { plugin: UnknownPluginInitializer } | undefined; + }; +} + +/** + * Reads the plugin's bundle declared in the global context. + */ +export function read(name: string) { + const coreWindow = (window as unknown) as CoreWindow; + const exportId = `plugin/${name}`; + const pluginExport = coreWindow.__kbnBundles__[exportId]; + if (!pluginExport) { + throw new Error(`Definition of plugin "${name}" not found and may have failed to load.`); + } else if (typeof pluginExport.plugin !== 'function') { + throw new Error(`Definition of plugin "${name}" should be a function.`); + } else { + return pluginExport.plugin; + } +} diff --git a/src/core/public/plugins/plugins_service.test.mocks.ts b/src/core/public/plugins/plugins_service.test.mocks.ts index a76078932518f..85b84e561056a 100644 --- a/src/core/public/plugins/plugins_service.test.mocks.ts +++ b/src/core/public/plugins/plugins_service.test.mocks.ts @@ -19,16 +19,16 @@ import { PluginName } from 'kibana/server'; import { Plugin } from './plugin'; -import { loadPluginBundleMock } from './plugin_loader.mock'; export type MockedPluginInitializer = jest.Mock>, any>; export const mockPluginInitializerProvider: jest.Mock< MockedPluginInitializer, [PluginName] -> = jest.fn().mockRejectedValue(new Error('No provider specified')); +> = jest.fn().mockImplementation(() => () => { + throw new Error('No provider specified'); +}); -export const mockLoadPluginBundle = loadPluginBundleMock.create(mockPluginInitializerProvider); -jest.mock('./plugin_loader', () => ({ - loadPluginBundle: mockLoadPluginBundle, +jest.mock('./plugin_reader', () => ({ + read: mockPluginInitializerProvider, })); diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 688eaf4f2bfc7..6d71844bc19c8 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -21,7 +21,6 @@ import { omit, pick } from 'lodash'; import { MockedPluginInitializer, - mockLoadPluginBundle, mockPluginInitializerProvider, } from './plugins_service.test.mocks'; @@ -32,6 +31,7 @@ import { PluginsServiceStartDeps, PluginsServiceSetupDeps, } from './plugins_service'; + import { InjectedPluginMetadata } from '../injected_metadata'; import { notificationServiceMock } from '../notifications/notifications_service.mock'; import { applicationServiceMock } from '../application/application_service.mock'; @@ -152,10 +152,6 @@ describe('PluginsService', () => { ] as unknown) as [[PluginName, any]]); }); - afterEach(() => { - mockLoadPluginBundle.mockClear(); - }); - describe('#getOpaqueIds()', () => { it('returns dependency tree of symbols', () => { const pluginsService = new PluginsService(mockCoreContext, plugins); @@ -174,15 +170,6 @@ describe('PluginsService', () => { }); describe('#setup()', () => { - it('fails if any bundle cannot be loaded', async () => { - mockLoadPluginBundle.mockRejectedValueOnce(new Error('Could not load bundle')); - - const pluginsService = new PluginsService(mockCoreContext, plugins); - await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Could not load bundle"` - ); - }); - it('fails if any plugin instance does not have a setup function', async () => { mockPluginInitializers.set('pluginA', (() => ({})) as any); const pluginsService = new PluginsService(mockCoreContext, plugins); @@ -191,25 +178,6 @@ describe('PluginsService', () => { ); }); - it('calls loadPluginBundles with http and plugins', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - - expect(mockLoadPluginBundle).toHaveBeenCalledTimes(3); - expect(mockLoadPluginBundle).toHaveBeenCalledWith( - mockSetupDeps.http.basePath.prepend, - 'pluginA' - ); - expect(mockLoadPluginBundle).toHaveBeenCalledWith( - mockSetupDeps.http.basePath.prepend, - 'pluginB' - ); - expect(mockLoadPluginBundle).toHaveBeenCalledWith( - mockSetupDeps.http.basePath.prepend, - 'pluginC' - ); - }); - it('initializes plugins with PluginInitializerContext', async () => { const pluginsService = new PluginsService(mockCoreContext, plugins); await pluginsService.setup(mockSetupDeps); @@ -302,7 +270,6 @@ describe('PluginsService', () => { const pluginsService = new PluginsService(mockCoreContext, plugins); const promise = pluginsService.setup(mockSetupDeps); - jest.runAllTimers(); // load plugin bundles await flushPromises(); jest.runAllTimers(); // setup plugins diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index e698af689036d..862aa5043ad4b 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -93,9 +93,6 @@ export class PluginsService implements CoreService { - // Load plugin bundles - await this.loadPluginBundles(deps.http.basePath.prepend); - // Setup each plugin with required and optional plugin contracts const contracts = new Map(); for (const [pluginName, plugin] of this.plugins.entries()) { @@ -167,9 +164,4 @@ export class PluginsService implements CoreService string) { - // Load all bundles in parallel - return Promise.all([...this.plugins.values()].map(plugin => plugin.load(addBasePath))); - } } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index b92bb209d2607..af06b207889c2 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -36,6 +36,7 @@ export interface AppBase { capabilities?: Partial; category?: AppCategory; chromeless?: boolean; + defaultPath?: string; euiIconType?: string; icon?: string; id: string; @@ -168,7 +169,7 @@ export enum AppStatus { export type AppUnmount = () => void; // @public -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick; // @public export type AppUpdater = (app: AppBase) => Partial | undefined; @@ -290,7 +291,6 @@ export interface ChromeNavLink { readonly subUrlBase?: string; readonly title: string; readonly tooltip?: string; - // @deprecated readonly url?: string; } diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index 27db79bb94d25..4fb433b5c77ba 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -1068,6 +1068,14 @@ describe('setup contract', () => { await create(); expect(create()).rejects.toThrowError('A cookieSessionStorageFactory was already created'); }); + + it('does not throw if called after stop', async () => { + const { createCookieSessionStorageFactory } = await server.setup(config); + await server.stop(); + expect(() => { + createCookieSessionStorageFactory(cookieOptions); + }).not.toThrow(); + }); }); describe('#isTlsEnabled', () => { @@ -1113,4 +1121,54 @@ describe('setup contract', () => { expect(getServerInfo().protocol).toEqual('https'); }); }); + + describe('#registerStaticDir', () => { + it('does not throw if called after stop', async () => { + const { registerStaticDir } = await server.setup(config); + await server.stop(); + expect(() => { + registerStaticDir('/path1/{path*}', '/path/to/resource'); + }).not.toThrow(); + }); + }); + + describe('#registerOnPreAuth', () => { + test('does not throw if called after stop', async () => { + const { registerOnPreAuth } = await server.setup(config); + await server.stop(); + expect(() => { + registerOnPreAuth((req, res) => res.unauthorized()); + }).not.toThrow(); + }); + }); + + describe('#registerOnPostAuth', () => { + test('does not throw if called after stop', async () => { + const { registerOnPostAuth } = await server.setup(config); + await server.stop(); + expect(() => { + registerOnPostAuth((req, res) => res.unauthorized()); + }).not.toThrow(); + }); + }); + + describe('#registerOnPreResponse', () => { + test('does not throw if called after stop', async () => { + const { registerOnPreResponse } = await server.setup(config); + await server.stop(); + expect(() => { + registerOnPreResponse((req, res, t) => t.next()); + }).not.toThrow(); + }); + }); + + describe('#registerAuth', () => { + test('does not throw if called after stop', async () => { + const { registerAuth } = await server.setup(config); + await server.stop(); + expect(() => { + registerAuth((req, res) => res.unauthorized()); + }).not.toThrow(); + }); + }); }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 77d3d99fb48cb..92ac5220735a1 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -74,6 +74,7 @@ export class HttpServer { private registeredRouters = new Set(); private authRegistered = false; private cookieSessionStorageCreated = false; + private stopped = false; private readonly log: Logger; private readonly authState: AuthStateStorage; @@ -144,6 +145,10 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Http server is not setup up yet'); } + if (this.stopped) { + this.log.warn(`start called after stop`); + return; + } this.log.debug('starting http server'); for (const router of this.registeredRouters) { @@ -189,13 +194,13 @@ export class HttpServer { } public async stop() { + this.stopped = true; if (this.server === undefined) { return; } this.log.debug('stopping http server'); await this.server.stop(); - this.server = undefined; } private getAuthOption( @@ -234,6 +239,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`setupConditionalCompression called after stop`); + } const { enabled, referrerWhitelist: list } = config.compression; if (!enabled) { @@ -261,6 +269,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`registerOnPostAuth called after stop`); + } this.server.ext('onPostAuth', adoptToHapiOnPostAuthFormat(fn, this.log)); } @@ -269,6 +280,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`registerOnPreAuth called after stop`); + } this.server.ext('onRequest', adoptToHapiOnPreAuthFormat(fn, this.log)); } @@ -277,6 +291,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`registerOnPreResponse called after stop`); + } this.server.ext('onPreResponse', adoptToHapiOnPreResponseFormat(fn, this.log)); } @@ -288,6 +305,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`createCookieSessionStorageFactory called after stop`); + } if (this.cookieSessionStorageCreated) { throw new Error('A cookieSessionStorageFactory was already created'); } @@ -305,6 +325,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`registerAuth called after stop`); + } if (this.authRegistered) { throw new Error('Auth interceptor was already registered'); } @@ -348,6 +371,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Http server is not setup up yet'); } + if (this.stopped) { + this.log.warn(`registerStaticDir called after stop`); + } this.server.route({ path, diff --git a/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts b/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts index 1790b096a71ae..dfa2396d5904b 100644 --- a/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts +++ b/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts @@ -48,7 +48,7 @@ describe('logLegacyThirdPartyPluginDeprecationWarning', () => { expect(log.warn).toHaveBeenCalledTimes(1); expect(log.warn.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "Some installed third party plugin(s) [plugin] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://www.elastic.co/guide/en/kibana/master/breaking-changes-8.0.html for a list of breaking changes and https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md for documentation on how to migrate legacy plugins.", + "Some installed third party plugin(s) [plugin] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://ela.st/kibana-breaking-changes-8-0 for a list of breaking changes and https://ela.st/kibana-platform-migration for documentation on how to migrate legacy plugins.", ] `); }); @@ -65,7 +65,7 @@ describe('logLegacyThirdPartyPluginDeprecationWarning', () => { expect(log.warn).toHaveBeenCalledTimes(1); expect(log.warn.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "Some installed third party plugin(s) [pluginA, pluginB, pluginC] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://www.elastic.co/guide/en/kibana/master/breaking-changes-8.0.html for a list of breaking changes and https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md for documentation on how to migrate legacy plugins.", + "Some installed third party plugin(s) [pluginA, pluginB, pluginC] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://ela.st/kibana-breaking-changes-8-0 for a list of breaking changes and https://ela.st/kibana-platform-migration for documentation on how to migrate legacy plugins.", ] `); }); diff --git a/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts b/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts index f9c3dcbf554cb..df86f5a2b4031 100644 --- a/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts +++ b/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts @@ -22,9 +22,10 @@ import { LegacyPluginSpec } from '../types'; const internalPaths = ['/src/legacy/core_plugins', '/x-pack']; -const breakingChangesUrl = - 'https://www.elastic.co/guide/en/kibana/master/breaking-changes-8.0.html'; -const migrationGuideUrl = 'https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md'; +// Use shortened URLs so destinations can be updated if/when documentation moves +// All platform team members have access to edit these +const breakingChangesUrl = 'https://ela.st/kibana-breaking-changes-8-0'; +const migrationGuideUrl = 'https://ela.st/kibana-platform-migration'; export const logLegacyThirdPartyPluginDeprecationWarning = ({ specs, diff --git a/src/core/server/rendering/views/template.tsx b/src/core/server/rendering/views/template.tsx index b38259a84cb9c..73e119a5a97e7 100644 --- a/src/core/server/rendering/views/template.tsx +++ b/src/core/server/rendering/views/template.tsx @@ -104,6 +104,10 @@ export const Template: FunctionComponent = ({ + + {/* Inject stylesheets into the before scripts so that KP plugins with bundled styles will override them */} + + {createElement('kbn-csp', { diff --git a/src/core/server/saved_objects/migrations/core/index.ts b/src/core/server/saved_objects/migrations/core/index.ts index 466d399f653cd..f7274740ea5fe 100644 --- a/src/core/server/saved_objects/migrations/core/index.ts +++ b/src/core/server/saved_objects/migrations/core/index.ts @@ -21,5 +21,5 @@ export { DocumentMigrator } from './document_migrator'; export { IndexMigrator } from './index_migrator'; export { buildActiveMappings } from './build_active_mappings'; export { CallCluster } from './call_cluster'; -export { LogFn } from './migration_logger'; +export { LogFn, SavedObjectsMigrationLogger } from './migration_logger'; export { MigrationResult, MigrationStatus } from './migration_coordinator'; diff --git a/src/core/server/saved_objects/migrations/mocks.ts b/src/core/server/saved_objects/migrations/mocks.ts new file mode 100644 index 0000000000000..76a890d26bfa0 --- /dev/null +++ b/src/core/server/saved_objects/migrations/mocks.ts @@ -0,0 +1,43 @@ +/* + * 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 { SavedObjectMigrationContext } from './types'; +import { SavedObjectsMigrationLogger } from './core'; + +const createLoggerMock = (): jest.Mocked => { + const mock = { + debug: jest.fn(), + info: jest.fn(), + warning: jest.fn(), + warn: jest.fn(), + }; + + return mock; +}; + +const createContextMock = (): jest.Mocked => { + const mock = { + log: createLoggerMock(), + }; + return mock; +}; + +export const migrationMocks = { + createContext: createContextMock, +}; diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts index 6bc085dde872e..85f15b4c18b66 100644 --- a/src/core/server/saved_objects/migrations/types.ts +++ b/src/core/server/saved_objects/migrations/types.ts @@ -26,23 +26,37 @@ import { SavedObjectsMigrationLogger } from './core/migration_logger'; * * @example * ```typescript - * const migrateProperty: SavedObjectMigrationFn = (doc, { log }) => { - * if(doc.attributes.someProp === null) { - * log.warn('Skipping migration'); - * } else { - * doc.attributes.someProp = migrateProperty(doc.attributes.someProp); - * } + * interface TypeV1Attributes { + * someKey: string; + * obsoleteProperty: number; + * } * - * return doc; + * interface TypeV2Attributes { + * someKey: string; + * newProperty: string; * } + * + * const migrateToV2: SavedObjectMigrationFn = (doc, { log }) => { + * const { obsoleteProperty, ...otherAttributes } = doc.attributes; + * // instead of mutating `doc` we make a shallow copy so that we can use separate types for the input + * // and output attributes. We don't need to make a deep copy, we just need to ensure that obsolete + * // attributes are not present on the returned doc. + * return { + * ...doc, + * attributes: { + * ...otherAttributes, + * newProperty: migrate(obsoleteProperty), + * }, + * }; + * }; * ``` * * @public */ -export type SavedObjectMigrationFn = ( - doc: SavedObjectUnsanitizedDoc, +export type SavedObjectMigrationFn = ( + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext -) => SavedObjectUnsanitizedDoc; +) => SavedObjectUnsanitizedDoc; /** * Migration context provided when invoking a {@link SavedObjectMigrationFn | migration handler} diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index 7ba4613c857d7..4e1f5981d6a41 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -31,6 +31,7 @@ import { savedObjectsClientProviderMock } from './service/lib/scoped_client_prov import { savedObjectsRepositoryMock } from './service/lib/repository.mock'; import { savedObjectsClientMock } from './service/saved_objects_client.mock'; import { typeRegistryMock } from './saved_objects_type_registry.mock'; +import { migrationMocks } from './migrations/mocks'; import { ServiceStatusLevels } from '../status'; type SavedObjectsServiceContract = PublicMethodsOf; @@ -105,4 +106,5 @@ export const savedObjectsServiceMock = { createSetupContract: createSetupContractMock, createInternalStartContract: createInternalStartContractMock, createStartContract: createStartContractMock, + createMigrationContext: migrationMocks.createContext, }; diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index a33e16895078e..acd2c7b5284aa 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -47,8 +47,8 @@ export interface SavedObjectsRawDocSource { /** * Saved Object base document */ -interface SavedObjectDoc { - attributes: any; +interface SavedObjectDoc { + attributes: T; id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional type: string; namespace?: string; @@ -69,7 +69,7 @@ interface Referencable { * * @public */ -export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; +export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; /** * Describes Saved Object documents that have passed through the migration @@ -77,4 +77,4 @@ export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; * * @public */ -export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; +export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index dc1c9d379d508..e8b77a8570291 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1680,7 +1680,7 @@ export interface SavedObjectMigrationContext { } // @public -export type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; +export type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; // @public export interface SavedObjectMigrationMap { @@ -1708,7 +1708,7 @@ export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOpti // Warning: (ae-forgotten-export) The symbol "Referencable" needs to be exported by the entry point index.d.ts // // @public -export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; +export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; // @public (undocumented) export interface SavedObjectsBaseOptions { @@ -2311,7 +2311,7 @@ export class SavedObjectTypeRegistry { } // @public -export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; +export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; // @public export type ScopeableRequest = KibanaRequest | LegacyRequest | FakeRequest; diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 43a2cbd78c502..c5387590fcf66 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -40,6 +40,7 @@ export default { ], collectCoverageFrom: [ 'src/plugins/**/*.{ts,tsx}', + '!src/plugins/**/{__test__,__snapshots__,__examples__,mocks,tests}/**/*', '!src/plugins/**/*.d.ts', 'packages/kbn-ui-framework/src/components/**/*.js', '!packages/kbn-ui-framework/src/components/index.js', diff --git a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index 6ccbc13aeeb57..6366466103652 100644 --- a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts +++ b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts @@ -21,6 +21,8 @@ // these are necessary to bootstrap the local angular. // They can stay even after NP cutover import angular from 'angular'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { EuiIcon } from '@elastic/eui'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { CoreStart, LegacyCoreStart } from 'kibana/public'; diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js index 7f5c7d4664af8..80ffa440e7285 100644 --- a/src/legacy/core_plugins/timelion/public/app.js +++ b/src/legacy/core_plugins/timelion/public/app.js @@ -18,6 +18,8 @@ */ import _ from 'lodash'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { i18n } from '@kbn/i18n'; diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 0d2f3528c9019..02491ff872981 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -153,6 +153,7 @@ export default class KbnServer { public server: Server; public inject: Server['inject']; public pluginSpecs: any[]; + public uiBundles: any; constructor( settings: Record, diff --git a/src/legacy/ui/public/angular_ui_select.js b/src/legacy/ui/public/angular_ui_select.js index 92b1bf53520ce..99f92587507c9 100644 --- a/src/legacy/ui/public/angular_ui_select.js +++ b/src/legacy/ui/public/angular_ui_select.js @@ -19,6 +19,7 @@ import 'jquery'; import 'angular'; +// required for `ngSanitize` angular module import 'angular-sanitize'; import 'ui-select/dist/select'; diff --git a/src/legacy/ui/public/i18n/index.test.tsx b/src/legacy/ui/public/i18n/index.test.tsx index c7a778ac18bd3..be8ab4cf8d696 100644 --- a/src/legacy/ui/public/i18n/index.test.tsx +++ b/src/legacy/ui/public/i18n/index.test.tsx @@ -21,6 +21,7 @@ import { render } from 'enzyme'; import PropTypes from 'prop-types'; import React from 'react'; +jest.mock('angular-sanitize', () => {}); jest.mock('ui/new_platform', () => ({ npStart: { core: { diff --git a/src/legacy/ui/public/i18n/index.tsx b/src/legacy/ui/public/i18n/index.tsx index c918554563fcb..6f1120dce0c7c 100644 --- a/src/legacy/ui/public/i18n/index.tsx +++ b/src/legacy/ui/public/i18n/index.tsx @@ -18,6 +18,8 @@ */ import React from 'react'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; // @ts-ignore diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index 8a71c6ccb1506..b60442690af3c 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -29,6 +29,7 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { document.body.innerHTML = err.outerHTML; } + var stylesheetTarget = document.querySelector('head meta[name="add-styles-here"]') function loadStyleSheet(url, cb) { var dom = document.createElement('link'); dom.rel = 'stylesheet'; @@ -36,9 +37,10 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { dom.href = url; dom.addEventListener('error', failure); dom.addEventListener('load', cb); - document.head.appendChild(dom); + document.head.insertBefore(dom, stylesheetTarget); } + var scriptsTarget = document.querySelector('head meta[name="add-scripts-here"]') function loadScript(url, cb) { var dom = document.createElement('script'); {{!-- NOTE: async = false is used to trigger async-download/ordered-execution as outlined here: https://www.html5rocks.com/en/tutorials/speed/script-loading/ --}} @@ -46,7 +48,7 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { dom.src = url; dom.addEventListener('error', failure); dom.addEventListener('load', cb); - document.head.appendChild(dom); + document.head.insertBefore(dom, scriptsTarget); } function load(urls, cb) { diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 801eecf5b608b..9b44395fa9c68 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -19,13 +19,15 @@ import { createHash } from 'crypto'; import Boom from 'boom'; -import { resolve } from 'path'; +import Path from 'path'; import { i18n } from '@kbn/i18n'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { AppBootstrap } from './bootstrap'; import { getApmConfig } from '../apm'; import { DllCompiler } from '../../../optimize/dynamic_dll_plugin'; +const uniq = (...items) => Array.from(new Set(items)); + /** * @typedef {import('../../server/kbn_server').default} KbnServer * @typedef {import('../../server/kbn_server').ResponseToolkit} ResponseToolkit @@ -39,7 +41,7 @@ import { DllCompiler } from '../../../optimize/dynamic_dll_plugin'; */ export function uiRenderMixin(kbnServer, server, config) { // render all views from ./views - server.setupViews(resolve(__dirname, 'views')); + server.setupViews(Path.resolve(__dirname, 'views')); const translationsCache = { translations: null, hash: null }; server.route({ @@ -94,9 +96,12 @@ export function uiRenderMixin(kbnServer, server, config) { ? await uiSettings.get('theme:darkMode') : false; + const buildHash = server.newPlatform.env.packageInfo.buildNum; const basePath = config.get('server.basePath'); - const regularBundlePath = `${basePath}/bundles`; - const dllBundlePath = `${basePath}/built_assets/dlls`; + + const regularBundlePath = `${basePath}/${buildHash}/bundles`; + const dllBundlePath = `${basePath}/${buildHash}/built_assets/dlls`; + const dllStyleChunks = DllCompiler.getRawDllConfig().chunks.map( chunk => `${dllBundlePath}/vendors${chunk}.style.dll.css` ); @@ -106,15 +111,15 @@ export function uiRenderMixin(kbnServer, server, config) { const styleSheetPaths = [ ...(isCore ? [] : dllStyleChunks), - `${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, + `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, ...(darkMode ? [ - `${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}`, + `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}`, `${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`, `${regularBundlePath}/dark_theme.style.css`, ] : [ - `${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`, + `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`, `${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`, `${regularBundlePath}/light_theme.style.css`, ]), @@ -129,13 +134,23 @@ export function uiRenderMixin(kbnServer, server, config) { ) .map(path => path.localPath.endsWith('.scss') - ? `${basePath}/built_assets/css/${path.publicPath}` + ? `${basePath}/${buildHash}/built_assets/css/${path.publicPath}` : `${basePath}/${path.publicPath}` ) .reverse(), ]), ]; + const kpPluginIds = uniq( + // load these plugins first, they are "shared" and other bundles access their + // public/index exports without considering topographic sorting by plugin deps (for now) + 'kibanaUtils', + 'kibanaReact', + 'data', + 'esUiShared', + ...kbnServer.newPlatform.__internals.uiPlugins.public.keys() + ); + const jsDependencyPaths = [ ...UiSharedDeps.jsDepFilenames.map( filename => `${regularBundlePath}/kbn-ui-shared-deps/${filename}` @@ -148,20 +163,22 @@ export function uiRenderMixin(kbnServer, server, config) { ...dllJsChunks, `${regularBundlePath}/commons.bundle.js`, ]), - `${regularBundlePath}/plugin/kibanaUtils/kibanaUtils.plugin.js`, - `${regularBundlePath}/plugin/esUiShared/esUiShared.plugin.js`, - `${regularBundlePath}/plugin/kibanaReact/kibanaReact.plugin.js`, - ]; - const uiPluginIds = [...kbnServer.newPlatform.__internals.uiPlugins.public.keys()]; + ...kpPluginIds.map( + pluginId => `${regularBundlePath}/plugin/${pluginId}/${pluginId}.plugin.js` + ), + ]; // These paths should align with the bundle routes configured in - // src/optimize/bundles_route/bundles_route.js + // src/optimize/bundles_route/bundles_route.ts const publicPathMap = JSON.stringify({ core: `${regularBundlePath}/core/`, 'kbn-ui-shared-deps': `${regularBundlePath}/kbn-ui-shared-deps/`, - ...uiPluginIds.reduce( - (acc, pluginId) => ({ ...acc, [pluginId]: `${regularBundlePath}/plugin/${pluginId}/` }), + ...kpPluginIds.reduce( + (acc, pluginId) => ({ + ...acc, + [pluginId]: `${regularBundlePath}/plugin/${pluginId}/`, + }), {} ), }); diff --git a/src/optimize/bundles_route/__tests__/bundles_route.js b/src/optimize/bundles_route/__tests__/bundles_route.js index 0b2aeda11fb0e..902fa59b20569 100644 --- a/src/optimize/bundles_route/__tests__/bundles_route.js +++ b/src/optimize/bundles_route/__tests__/bundles_route.js @@ -32,6 +32,7 @@ import { PUBLIC_PATH_PLACEHOLDER } from '../../public_path_placeholder'; const chance = new Chance(); const outputFixture = resolve(__dirname, './fixtures/output'); +const pluginNoPlaceholderFixture = resolve(__dirname, './fixtures/plugin/no_placeholder'); const randomWordsCache = new Set(); const uniqueRandomWord = () => { @@ -58,6 +59,9 @@ describe('optimizer/bundle route', () => { dllBundlesPath = outputFixture, basePublicPath = '', builtCssPath = outputFixture, + npUiPluginPublicDirs = [], + buildHash = '1234', + isDist = false, } = options; const server = new Hapi.Server(); @@ -69,6 +73,9 @@ describe('optimizer/bundle route', () => { dllBundlesPath, basePublicPath, builtCssPath, + npUiPluginPublicDirs, + buildHash, + isDist, }) ); @@ -158,7 +165,7 @@ describe('optimizer/bundle route', () => { it('responds with exact file data', async () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/image.png', + url: '/1234/bundles/image.png', }); expect(response.statusCode).to.be(200); @@ -173,7 +180,7 @@ describe('optimizer/bundle route', () => { it('responds with no content-length and exact file data', async () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }); expect(response.statusCode).to.be(200); @@ -187,12 +194,12 @@ describe('optimizer/bundle route', () => { }); describe('js file with placeholder', () => { - it('responds with no content-length and modified file data', async () => { + it('responds with no content-length and modifiedfile data ', async () => { const basePublicPath = `/${uniqueRandomWord()}`; const server = createServer({ basePublicPath }); const response = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', }); expect(response.statusCode).to.be(200); @@ -204,7 +211,7 @@ describe('optimizer/bundle route', () => { ); expect(response.result.indexOf(source)).to.be(-1); expect(response.result).to.be( - replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/bundles/`) + replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/1234/bundles/`) ); }); }); @@ -213,7 +220,7 @@ describe('optimizer/bundle route', () => { it('responds with no content-length and exact file data', async () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/no_placeholder.css', + url: '/1234/bundles/no_placeholder.css', }); expect(response.statusCode).to.be(200); @@ -231,7 +238,7 @@ describe('optimizer/bundle route', () => { const server = createServer({ basePublicPath }); const response = await server.inject({ - url: '/bundles/with_placeholder.css', + url: '/1234/bundles/with_placeholder.css', }); expect(response.statusCode).to.be(200); @@ -240,7 +247,7 @@ describe('optimizer/bundle route', () => { expect(response.headers).to.have.property('content-type', 'text/css; charset=utf-8'); expect(response.result.indexOf(source)).to.be(-1); expect(response.result).to.be( - replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/bundles/`) + replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/1234/bundles/`) ); }); }); @@ -250,7 +257,7 @@ describe('optimizer/bundle route', () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/../outside_output.js', + url: '/1234/bundles/../outside_output.js', }); expect(response.statusCode).to.be(404); @@ -267,7 +274,7 @@ describe('optimizer/bundle route', () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/non_existent.js', + url: '/1234/bundles/non_existent.js', }); expect(response.statusCode).to.be(404); @@ -286,7 +293,7 @@ describe('optimizer/bundle route', () => { }); const response = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', }); expect(response.statusCode).to.be(404); @@ -306,7 +313,7 @@ describe('optimizer/bundle route', () => { sinon.assert.notCalled(createHash); const resp1 = await server.inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }); sinon.assert.calledOnce(createHash); @@ -314,23 +321,23 @@ describe('optimizer/bundle route', () => { expect(resp1.statusCode).to.be(200); const resp2 = await server.inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }); sinon.assert.notCalled(createHash); expect(resp2.statusCode).to.be(200); }); - it('is unique per basePublicPath although content is the same', async () => { + it('is unique per basePublicPath although content is the same (by default)', async () => { const basePublicPath1 = `/${uniqueRandomWord()}`; const basePublicPath2 = `/${uniqueRandomWord()}`; const [resp1, resp2] = await Promise.all([ createServer({ basePublicPath: basePublicPath1 }).inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }), createServer({ basePublicPath: basePublicPath2 }).inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }), ]); @@ -349,13 +356,13 @@ describe('optimizer/bundle route', () => { it('responds with 304 when etag and last modified are sent back', async () => { const server = createServer(); const resp = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', }); expect(resp.statusCode).to.be(200); const resp2 = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', headers: { 'if-modified-since': resp.headers['last-modified'], 'if-none-match': resp.headers.etag, @@ -366,4 +373,80 @@ describe('optimizer/bundle route', () => { expect(resp2.result).to.have.length(0); }); }); + + describe('kibana platform assets', () => { + describe('caching', () => { + describe('for non-distributable mode', () => { + it('uses "etag" header to invalidate cache', async () => { + const basePublicPath = `/${uniqueRandomWord()}`; + + const npUiPluginPublicDirs = [ + { + id: 'no_placeholder', + path: pluginNoPlaceholderFixture, + }, + ]; + const responce = await createServer({ basePublicPath, npUiPluginPublicDirs }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }); + + expect(responce.statusCode).to.be(200); + + expect(responce.headers.etag).to.be.a('string'); + expect(responce.headers['cache-control']).to.be('must-revalidate'); + }); + + it('creates the same "etag" header for the same content with the same basePath', async () => { + const npUiPluginPublicDirs = [ + { + id: 'no_placeholder', + path: pluginNoPlaceholderFixture, + }, + ]; + const [resp1, resp2] = await Promise.all([ + createServer({ basePublicPath: '', npUiPluginPublicDirs }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }), + createServer({ basePublicPath: '', npUiPluginPublicDirs }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }), + ]); + + expect(resp1.statusCode).to.be(200); + expect(resp2.statusCode).to.be(200); + + expect(resp1.rawPayload).to.eql(resp2.rawPayload); + + expect(resp1.headers.etag).to.be.a('string'); + expect(resp2.headers.etag).to.be.a('string'); + expect(resp1.headers.etag).to.eql(resp2.headers.etag); + }); + }); + + describe('for distributable mode', () => { + it('commands to cache assets for each release for a year', async () => { + const basePublicPath = `/${uniqueRandomWord()}`; + + const npUiPluginPublicDirs = [ + { + id: 'no_placeholder', + path: pluginNoPlaceholderFixture, + }, + ]; + const responce = await createServer({ + basePublicPath, + npUiPluginPublicDirs, + isDist: true, + }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }); + + expect(responce.statusCode).to.be(200); + + expect(responce.headers.etag).to.be(undefined); + expect(responce.headers['cache-control']).to.be('max-age=31536000'); + }); + }); + }); + }); }); diff --git a/src/optimize/bundles_route/bundles_route.js b/src/optimize/bundles_route/bundles_route.ts similarity index 74% rename from src/optimize/bundles_route/bundles_route.js rename to src/optimize/bundles_route/bundles_route.ts index 4030988c8552c..e9cfba0130d95 100644 --- a/src/optimize/bundles_route/bundles_route.js +++ b/src/optimize/bundles_route/bundles_route.ts @@ -18,10 +18,13 @@ */ import { isAbsolute, extname, join } from 'path'; -import LruCache from 'lru-cache'; + +import Hapi from 'hapi'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; + import { createDynamicAssetResponse } from './dynamic_asset_response'; -import { assertIsNpUiPluginPublicDirs } from '../np_ui_plugin_public_dirs'; +import { FileHashCache } from './file_hash_cache'; +import { assertIsNpUiPluginPublicDirs, NpUiPluginPublicDirs } from '../np_ui_plugin_public_dirs'; import { fromRoot } from '../../core/server/utils'; /** @@ -44,11 +47,21 @@ export function createBundlesRoute({ basePublicPath, builtCssPath, npUiPluginPublicDirs = [], + buildHash, + isDist = false, +}: { + regularBundlesPath: string; + dllBundlesPath: string; + basePublicPath: string; + builtCssPath: string; + npUiPluginPublicDirs?: NpUiPluginPublicDirs; + buildHash: string; + isDist?: boolean; }) { // rather than calculate the fileHash on every request, we // provide a cache object to `resolveDynamicAssetResponse()` that // will store the 100 most recently used hashes. - const fileHashCache = new LruCache(100); + const fileHashCache = new FileHashCache(); assertIsNpUiPluginPublicDirs(npUiPluginPublicDirs); if (typeof regularBundlesPath !== 'string' || !isAbsolute(regularBundlesPath)) { @@ -73,45 +86,51 @@ export function createBundlesRoute({ return [ buildRouteForBundles({ - publicPath: `${basePublicPath}/bundles/kbn-ui-shared-deps/`, - routePath: '/bundles/kbn-ui-shared-deps/', + publicPath: `${basePublicPath}/${buildHash}/bundles/kbn-ui-shared-deps/`, + routePath: `/${buildHash}/bundles/kbn-ui-shared-deps/`, bundlesPath: UiSharedDeps.distDir, fileHashCache, replacePublicPath: false, + isDist, }), ...npUiPluginPublicDirs.map(({ id, path }) => buildRouteForBundles({ - publicPath: `${basePublicPath}/bundles/plugin/${id}/`, - routePath: `/bundles/plugin/${id}/`, + publicPath: `${basePublicPath}/${buildHash}/bundles/plugin/${id}/`, + routePath: `/${buildHash}/bundles/plugin/${id}/`, bundlesPath: path, fileHashCache, replacePublicPath: false, + isDist, }) ), buildRouteForBundles({ - publicPath: `${basePublicPath}/bundles/core/`, - routePath: `/bundles/core/`, + publicPath: `${basePublicPath}/${buildHash}/bundles/core/`, + routePath: `/${buildHash}/bundles/core/`, bundlesPath: fromRoot(join('src', 'core', 'target', 'public')), fileHashCache, replacePublicPath: false, + isDist, }), buildRouteForBundles({ - publicPath: `${basePublicPath}/bundles/`, - routePath: '/bundles/', + publicPath: `${basePublicPath}/${buildHash}/bundles/`, + routePath: `/${buildHash}/bundles/`, bundlesPath: regularBundlesPath, fileHashCache, + isDist, }), buildRouteForBundles({ - publicPath: `${basePublicPath}/built_assets/dlls/`, - routePath: '/built_assets/dlls/', + publicPath: `${basePublicPath}/${buildHash}/built_assets/dlls/`, + routePath: `/${buildHash}/built_assets/dlls/`, bundlesPath: dllBundlesPath, fileHashCache, + isDist, }), buildRouteForBundles({ publicPath: `${basePublicPath}/`, - routePath: '/built_assets/css/', + routePath: `/${buildHash}/built_assets/css/`, bundlesPath: builtCssPath, fileHashCache, + isDist, }), ]; } @@ -122,6 +141,14 @@ function buildRouteForBundles({ bundlesPath, fileHashCache, replacePublicPath = true, + isDist, +}: { + publicPath: string; + routePath: string; + bundlesPath: string; + fileHashCache: FileHashCache; + replacePublicPath?: boolean; + isDist: boolean; }) { return { method: 'GET', @@ -130,7 +157,7 @@ function buildRouteForBundles({ auth: false, ext: { onPreHandler: { - method(request, h) { + method(request: Hapi.Request, h: Hapi.ResponseToolkit) { const ext = extname(request.params.path); if (ext !== '.js' && ext !== '.css') { @@ -144,6 +171,7 @@ function buildRouteForBundles({ fileHashCache, publicPath, replacePublicPath, + isDist, }); }, }, diff --git a/src/optimize/bundles_route/dynamic_asset_response.js b/src/optimize/bundles_route/dynamic_asset_response.ts similarity index 67% rename from src/optimize/bundles_route/dynamic_asset_response.js rename to src/optimize/bundles_route/dynamic_asset_response.ts index 80c49a26270fd..a020c6935eeec 100644 --- a/src/optimize/bundles_route/dynamic_asset_response.js +++ b/src/optimize/bundles_route/dynamic_asset_response.ts @@ -17,15 +17,26 @@ * under the License. */ +import Fs from 'fs'; import { resolve } from 'path'; -import { open, fstat, createReadStream, close } from 'fs'; +import { promisify } from 'util'; import Boom from 'boom'; -import { fromNode as fcb } from 'bluebird'; +import Hapi from 'hapi'; +import { FileHashCache } from './file_hash_cache'; import { getFileHash } from './file_hash'; +// @ts-ignore import { replacePlaceholder } from '../public_path_placeholder'; +const MINUTE = 60; +const HOUR = 60 * MINUTE; +const DAY = 24 * HOUR; + +const asyncOpen = promisify(Fs.open); +const asyncClose = promisify(Fs.close); +const asyncFstat = promisify(Fs.fstat); + /** * Create a Hapi response for the requested path. This is designed * to replicate a subset of the features provided by Hapi's Inert @@ -44,55 +55,70 @@ import { replacePlaceholder } from '../public_path_placeholder'; * - cached hash/etag is based on the file on disk, but modified * by the public path so that individual public paths have * different etags, but can share a cache - * - * @param {Object} options - * @property {Hapi.Request} options.request - * @property {string} options.bundlesPath - * @property {string} options.publicPath - * @property {LruCache} options.fileHashCache */ -export async function createDynamicAssetResponse(options) { - const { request, h, bundlesPath, publicPath, fileHashCache, replacePublicPath } = options; +export async function createDynamicAssetResponse({ + request, + h, + bundlesPath, + publicPath, + fileHashCache, + replacePublicPath, + isDist, +}: { + request: Hapi.Request; + h: Hapi.ResponseToolkit; + bundlesPath: string; + publicPath: string; + fileHashCache: FileHashCache; + replacePublicPath: boolean; + isDist: boolean; +}) { + let fd: number | undefined; - let fd; try { const path = resolve(bundlesPath, request.params.path); // prevent path traversal, only process paths that resolve within bundlesPath if (!path.startsWith(bundlesPath)) { - throw Boom.forbidden(null, 'EACCES'); + throw Boom.forbidden(undefined, 'EACCES'); } // we use and manage a file descriptor mostly because // that's what Inert does, and since we are accessing // the file 2 or 3 times per request it seems logical - fd = await fcb(cb => open(path, 'r', cb)); + fd = await asyncOpen(path, 'r'); - const stat = await fcb(cb => fstat(fd, cb)); - const hash = await getFileHash(fileHashCache, path, stat, fd); + const stat = await asyncFstat(fd); + const hash = isDist ? undefined : await getFileHash(fileHashCache, path, stat, fd); - const read = createReadStream(null, { + const read = Fs.createReadStream(null as any, { fd, start: 0, autoClose: true, }); - fd = null; // read stream is now responsible for fd + fd = undefined; // read stream is now responsible for fd const content = replacePublicPath ? replacePlaceholder(read, publicPath) : read; - const etag = replacePublicPath ? `${hash}-${publicPath}` : hash; - return h + const response = h .response(content) .takeover() .code(200) - .etag(etag) - .header('cache-control', 'must-revalidate') .type(request.server.mime.path(path).type); + + if (isDist) { + response.header('cache-control', `max-age=${365 * DAY}`); + } else { + response.etag(`${hash}-${publicPath}`); + response.header('cache-control', 'must-revalidate'); + } + + return response; } catch (error) { if (fd) { try { - await fcb(cb => close(fd, cb)); - } catch (error) { + await asyncClose(fd); + } catch (_) { // ignore errors from close, we already have one to report // and it's very likely they are the same } diff --git a/src/optimize/bundles_route/file_hash.js b/src/optimize/bundles_route/file_hash.ts similarity index 73% rename from src/optimize/bundles_route/file_hash.js rename to src/optimize/bundles_route/file_hash.ts index d9464cf05eca1..7b0801098ed10 100644 --- a/src/optimize/bundles_route/file_hash.js +++ b/src/optimize/bundles_route/file_hash.ts @@ -18,20 +18,17 @@ */ import { createHash } from 'crypto'; -import { createReadStream } from 'fs'; +import Fs from 'fs'; import * as Rx from 'rxjs'; -import { merge, mergeMap, takeUntil } from 'rxjs/operators'; +import { takeUntil, map } from 'rxjs/operators'; + +import { FileHashCache } from './file_hash_cache'; /** * Get the hash of a file via a file descriptor - * @param {LruCache} cache - * @param {string} path - * @param {Fs.Stat} stat - * @param {Fs.FileDescriptor} fd - * @return {Promise} */ -export async function getFileHash(cache, path, stat, fd) { +export async function getFileHash(cache: FileHashCache, path: string, stat: Fs.Stats, fd: number) { const key = `${path}:${stat.ino}:${stat.size}:${stat.mtime.getTime()}`; const cached = cache.get(key); @@ -40,17 +37,21 @@ export async function getFileHash(cache, path, stat, fd) { } const hash = createHash('sha1'); - const read = createReadStream(null, { + const read = Fs.createReadStream(null as any, { fd, start: 0, autoClose: false, }); - const promise = Rx.fromEvent(read, 'data') - .pipe( - merge(Rx.fromEvent(read, 'error').pipe(mergeMap(Rx.throwError))), - takeUntil(Rx.fromEvent(read, 'end')) + const promise = Rx.merge( + Rx.fromEvent(read, 'data'), + Rx.fromEvent(read, 'error').pipe( + map(error => { + throw error; + }) ) + ) + .pipe(takeUntil(Rx.fromEvent(read, 'end'))) .forEach(chunk => hash.update(chunk)) .then(() => hash.digest('hex')) .catch(error => { diff --git a/src/optimize/bundles_route/file_hash_cache.ts b/src/optimize/bundles_route/file_hash_cache.ts new file mode 100644 index 0000000000000..a7cdabbff13a7 --- /dev/null +++ b/src/optimize/bundles_route/file_hash_cache.ts @@ -0,0 +1,36 @@ +/* + * 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 LruCache from 'lru-cache'; + +export class FileHashCache { + private lru = new LruCache>(100); + + get(key: string) { + return this.lru.get(key); + } + + set(key: string, value: Promise) { + this.lru.set(key, value); + } + + del(key: string) { + this.lru.del(key); + } +} diff --git a/src/optimize/bundles_route/index.js b/src/optimize/bundles_route/index.ts similarity index 100% rename from src/optimize/bundles_route/index.js rename to src/optimize/bundles_route/index.ts diff --git a/src/optimize/bundles_route/proxy_bundles_route.js b/src/optimize/bundles_route/proxy_bundles_route.ts similarity index 70% rename from src/optimize/bundles_route/proxy_bundles_route.js rename to src/optimize/bundles_route/proxy_bundles_route.ts index fff0ec444d95b..1d189054324a1 100644 --- a/src/optimize/bundles_route/proxy_bundles_route.js +++ b/src/optimize/bundles_route/proxy_bundles_route.ts @@ -17,15 +17,23 @@ * under the License. */ -export function createProxyBundlesRoute({ host, port }) { +export function createProxyBundlesRoute({ + host, + port, + buildHash, +}: { + host: string; + port: number; + buildHash: string; +}) { return [ - buildProxyRouteForBundles('/bundles/', host, port), - buildProxyRouteForBundles('/built_assets/dlls/', host, port), - buildProxyRouteForBundles('/built_assets/css/', host, port), + buildProxyRouteForBundles(`/${buildHash}/bundles/`, host, port), + buildProxyRouteForBundles(`/${buildHash}/built_assets/dlls/`, host, port), + buildProxyRouteForBundles(`/${buildHash}/built_assets/css/`, host, port), ]; } -function buildProxyRouteForBundles(routePath, host, port) { +function buildProxyRouteForBundles(routePath: string, host: string, port: number) { return { path: `${routePath}{path*}`, method: 'GET', diff --git a/src/optimize/index.js b/src/optimize/index.js index b7b9f7712358a..363f81a6a3a96 100644 --- a/src/optimize/index.js +++ b/src/optimize/index.js @@ -17,72 +17,5 @@ * under the License. */ -import FsOptimizer from './fs_optimizer'; -import { createBundlesRoute } from './bundles_route'; -import { DllCompiler } from './dynamic_dll_plugin'; -import { fromRoot } from '../core/server/utils'; -import { getNpUiPluginPublicDirs } from './np_ui_plugin_public_dirs'; - -export default async (kbnServer, server, config) => { - if (!config.get('optimize.enabled')) return; - - // the watch optimizer sets up two threads, one is the server listening - // on 5601 and the other is a server listening on 5602 that builds the - // bundles in a "middleware" style. - // - // the server listening on 5601 may be restarted a number of times, depending - // on the watch setup managed by the cli. It proxies all bundles/* and built_assets/dlls/* - // requests to the other server. The server on 5602 is long running, in order - // to prevent complete rebuilds of the optimize content. - const watch = config.get('optimize.watch'); - if (watch) { - return await kbnServer.mixin(require('./watch/watch')); - } - - const { uiBundles } = kbnServer; - server.route( - createBundlesRoute({ - regularBundlesPath: uiBundles.getWorkingDir(), - dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, - basePublicPath: config.get('server.basePath'), - builtCssPath: fromRoot('built_assets/css'), - npUiPluginPublicDirs: getNpUiPluginPublicDirs(kbnServer), - }) - ); - - // in prod, only bundle when something is missing or invalid - const reuseCache = config.get('optimize.useBundleCache') - ? await uiBundles.areAllBundleCachesValid() - : false; - - // we might not have any work to do - if (reuseCache) { - server.log(['debug', 'optimize'], `All bundles are cached and ready to go!`); - return; - } - - await uiBundles.resetBundleDir(); - - // only require the FsOptimizer when we need to - const optimizer = new FsOptimizer({ - logWithMetadata: (tags, message, metadata) => server.logWithMetadata(tags, message, metadata), - uiBundles, - profile: config.get('optimize.profile'), - sourceMaps: config.get('optimize.sourceMaps'), - workers: config.get('optimize.workers'), - }); - - server.log( - ['info', 'optimize'], - `Optimizing and caching ${uiBundles.getDescription()}. This may take a few minutes` - ); - - const start = Date.now(); - await optimizer.run(); - const seconds = ((Date.now() - start) / 1000).toFixed(2); - - server.log( - ['info', 'optimize'], - `Optimization of ${uiBundles.getDescription()} complete in ${seconds} seconds` - ); -}; +import { optimizeMixin } from './optimize_mixin'; +export default optimizeMixin; diff --git a/src/optimize/np_ui_plugin_public_dirs.js b/src/optimize/np_ui_plugin_public_dirs.ts similarity index 73% rename from src/optimize/np_ui_plugin_public_dirs.js rename to src/optimize/np_ui_plugin_public_dirs.ts index de05fd2b863b8..e7c3207948f6a 100644 --- a/src/optimize/np_ui_plugin_public_dirs.js +++ b/src/optimize/np_ui_plugin_public_dirs.ts @@ -16,8 +16,14 @@ * specific language governing permissions and limitations * under the License. */ +import KbnServer from '../legacy/server/kbn_server'; -export function getNpUiPluginPublicDirs(kbnServer) { +export type NpUiPluginPublicDirs = Array<{ + id: string; + path: string; +}>; + +export function getNpUiPluginPublicDirs(kbnServer: KbnServer): NpUiPluginPublicDirs { return Array.from(kbnServer.newPlatform.__internals.uiPlugins.internal.entries()).map( ([id, { publicTargetDir }]) => ({ id, @@ -26,17 +32,17 @@ export function getNpUiPluginPublicDirs(kbnServer) { ); } -export function isNpUiPluginPublicDirs(something) { +export function isNpUiPluginPublicDirs(x: any): x is NpUiPluginPublicDirs { return ( - Array.isArray(something) && - something.every( + Array.isArray(x) && + x.every( s => typeof s === 'object' && s && typeof s.id === 'string' && typeof s.path === 'string' ) ); } -export function assertIsNpUiPluginPublicDirs(something) { - if (!isNpUiPluginPublicDirs(something)) { +export function assertIsNpUiPluginPublicDirs(x: any): asserts x is NpUiPluginPublicDirs { + if (!isNpUiPluginPublicDirs(x)) { throw new TypeError( 'npUiPluginPublicDirs must be an array of objects with string `id` and `path` properties' ); diff --git a/src/optimize/optimize_mixin.ts b/src/optimize/optimize_mixin.ts new file mode 100644 index 0000000000000..9a3f08e2f667e --- /dev/null +++ b/src/optimize/optimize_mixin.ts @@ -0,0 +1,100 @@ +/* + * 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 Hapi from 'hapi'; + +// @ts-ignore not TS yet +import FsOptimizer from './fs_optimizer'; +import { createBundlesRoute } from './bundles_route'; +// @ts-ignore not TS yet +import { DllCompiler } from './dynamic_dll_plugin'; +import { fromRoot } from '../core/server/utils'; +import { getNpUiPluginPublicDirs } from './np_ui_plugin_public_dirs'; +import KbnServer, { KibanaConfig } from '../legacy/server/kbn_server'; + +export const optimizeMixin = async ( + kbnServer: KbnServer, + server: Hapi.Server, + config: KibanaConfig +) => { + if (!config.get('optimize.enabled')) return; + + // the watch optimizer sets up two threads, one is the server listening + // on 5601 and the other is a server listening on 5602 that builds the + // bundles in a "middleware" style. + // + // the server listening on 5601 may be restarted a number of times, depending + // on the watch setup managed by the cli. It proxies all bundles/* and built_assets/dlls/* + // requests to the other server. The server on 5602 is long running, in order + // to prevent complete rebuilds of the optimize content. + const watch = config.get('optimize.watch'); + if (watch) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return await kbnServer.mixin(require('./watch/watch')); + } + + const { uiBundles } = kbnServer; + server.route( + createBundlesRoute({ + regularBundlesPath: uiBundles.getWorkingDir(), + dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, + basePublicPath: config.get('server.basePath'), + builtCssPath: fromRoot('built_assets/css'), + npUiPluginPublicDirs: getNpUiPluginPublicDirs(kbnServer), + buildHash: kbnServer.newPlatform.env.packageInfo.buildNum.toString(), + isDist: kbnServer.newPlatform.env.packageInfo.dist, + }) + ); + + // in prod, only bundle when something is missing or invalid + const reuseCache = config.get('optimize.useBundleCache') + ? await uiBundles.areAllBundleCachesValid() + : false; + + // we might not have any work to do + if (reuseCache) { + server.log(['debug', 'optimize'], `All bundles are cached and ready to go!`); + return; + } + + await uiBundles.resetBundleDir(); + + // only require the FsOptimizer when we need to + const optimizer = new FsOptimizer({ + logWithMetadata: server.logWithMetadata, + uiBundles, + profile: config.get('optimize.profile'), + sourceMaps: config.get('optimize.sourceMaps'), + workers: config.get('optimize.workers'), + }); + + server.log( + ['info', 'optimize'], + `Optimizing and caching ${uiBundles.getDescription()}. This may take a few minutes` + ); + + const start = Date.now(); + await optimizer.run(); + const seconds = ((Date.now() - start) / 1000).toFixed(2); + + server.log( + ['info', 'optimize'], + `Optimization of ${uiBundles.getDescription()} complete in ${seconds} seconds` + ); +}; diff --git a/src/optimize/public_path_placeholder.js b/src/optimize/public_path_placeholder.ts similarity index 76% rename from src/optimize/public_path_placeholder.js rename to src/optimize/public_path_placeholder.ts index ef05d9e5ae704..1ec2b4a431aa6 100644 --- a/src/optimize/public_path_placeholder.js +++ b/src/optimize/public_path_placeholder.ts @@ -17,14 +17,20 @@ * under the License. */ -import { createReplaceStream } from '../legacy/utils'; +import Stream from 'stream'; +import Fs from 'fs'; import * as Rx from 'rxjs'; import { take, takeUntil } from 'rxjs/operators'; +import { createReplaceStream } from '../legacy/utils'; export const PUBLIC_PATH_PLACEHOLDER = '__REPLACE_WITH_PUBLIC_PATH__'; -export function replacePlaceholder(read, replacement) { +interface ClosableTransform extends Stream.Transform { + close(): void; +} + +export function replacePlaceholder(read: Stream.Readable, replacement: string) { const replace = createReplaceStream(PUBLIC_PATH_PLACEHOLDER, replacement); // handle errors on the read stream by proxying them @@ -37,13 +43,15 @@ export function replacePlaceholder(read, replacement) { replace.end(); }); - replace.close = () => { - read.unpipe(); + const closableReplace: ClosableTransform = Object.assign(replace, { + close: () => { + read.unpipe(); - if (read.close) { - read.close(); - } - }; + if ('close' in read) { + (read as Fs.ReadStream).close(); + } + }, + }); - return read.pipe(replace); + return read.pipe(closableReplace); } diff --git a/src/optimize/watch/optmzr_role.js b/src/optimize/watch/optmzr_role.js index a31ef7229e5da..1f6107996277c 100644 --- a/src/optimize/watch/optmzr_role.js +++ b/src/optimize/watch/optmzr_role.js @@ -49,7 +49,8 @@ export default async (kbnServer, kibanaHapiServer, config) => { config.get('optimize.watchPort'), config.get('server.basePath'), watchOptimizer, - getNpUiPluginPublicDirs(kbnServer) + getNpUiPluginPublicDirs(kbnServer), + kbnServer.newPlatform.env.packageInfo.buildNum.toString() ); watchOptimizer.status$.subscribe({ diff --git a/src/optimize/watch/proxy_role.js b/src/optimize/watch/proxy_role.js index 6093658ae1a2d..0f6f3b2d4b622 100644 --- a/src/optimize/watch/proxy_role.js +++ b/src/optimize/watch/proxy_role.js @@ -26,6 +26,7 @@ export default (kbnServer, server, config) => { createProxyBundlesRoute({ host: config.get('optimize.watchHost'), port: config.get('optimize.watchPort'), + buildHash: kbnServer.newPlatform.env.packageInfo.buildNum.toString(), }) ); diff --git a/src/optimize/watch/watch_optimizer.js b/src/optimize/watch/watch_optimizer.js index 6c20f21c7768e..cdff57a00c2e0 100644 --- a/src/optimize/watch/watch_optimizer.js +++ b/src/optimize/watch/watch_optimizer.js @@ -106,7 +106,7 @@ export default class WatchOptimizer extends BaseOptimizer { }); } - bindToServer(server, basePath, npUiPluginPublicDirs) { + bindToServer(server, basePath, npUiPluginPublicDirs, buildHash) { // pause all requests received while the compiler is running // and continue once an outcome is reached (aborting the request // with an error if it was a failure). @@ -118,6 +118,7 @@ export default class WatchOptimizer extends BaseOptimizer { server.route( createBundlesRoute({ npUiPluginPublicDirs: npUiPluginPublicDirs, + buildHash, regularBundlesPath: this.compiler.outputPath, dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, basePublicPath: basePath, diff --git a/src/optimize/watch/watch_server.js b/src/optimize/watch/watch_server.js index 74a96dc8aea6e..81e04a5b83956 100644 --- a/src/optimize/watch/watch_server.js +++ b/src/optimize/watch/watch_server.js @@ -21,10 +21,11 @@ import { Server } from 'hapi'; import { registerHapiPlugins } from '../../legacy/server/http/register_hapi_plugins'; export default class WatchServer { - constructor(host, port, basePath, optimizer, npUiPluginPublicDirs) { + constructor(host, port, basePath, optimizer, npUiPluginPublicDirs, buildHash) { this.basePath = basePath; this.optimizer = optimizer; this.npUiPluginPublicDirs = npUiPluginPublicDirs; + this.buildHash = buildHash; this.server = new Server({ host: host, port: port, @@ -35,7 +36,12 @@ export default class WatchServer { async init() { await this.optimizer.init(); - this.optimizer.bindToServer(this.server, this.basePath, this.npUiPluginPublicDirs); + this.optimizer.bindToServer( + this.server, + this.basePath, + this.npUiPluginPublicDirs, + this.buildHash + ); await this.server.start(); } } diff --git a/src/plugins/dashboard/public/application/application.ts b/src/plugins/dashboard/public/application/application.ts index a1696298117b0..37f014d836075 100644 --- a/src/plugins/dashboard/public/application/application.ts +++ b/src/plugins/dashboard/public/application/application.ts @@ -21,6 +21,8 @@ import './index.scss'; import { EuiIcon } from '@elastic/eui'; import angular, { IModule } from 'angular'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext, diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 5f6b67ee6ad20..7de054f2eaa9c 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -141,10 +141,14 @@ export class DashboardPlugin if (share) { share.urlGenerators.registerUrlGenerator( - createDirectAccessDashboardLinkGenerator(async () => ({ - appBasePath: (await startServices)[0].application.getUrlForApp('dashboard'), - useHashedUrl: (await startServices)[0].uiSettings.get('state:storeInSessionStorage'), - })) + createDirectAccessDashboardLinkGenerator(async () => { + const [coreStart, , selfStart] = await startServices; + return { + appBasePath: coreStart.application.getUrlForApp('dashboard'), + useHashedUrl: coreStart.uiSettings.get('state:storeInSessionStorage'), + savedDashboardLoader: selfStart.getSavedDashboardLoader(), + }; + }) ); } diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts index d48aacc1d8c1e..248a3f991d6cb 100644 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ b/src/plugins/dashboard/public/url_generator.test.ts @@ -21,10 +21,33 @@ import { createDirectAccessDashboardLinkGenerator } from './url_generator'; import { hashedItemStore } from '../../kibana_utils/public'; // eslint-disable-next-line import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; -import { esFilters } from '../../data/public'; +import { esFilters, Filter } from '../../data/public'; +import { SavedObjectLoader } from '../../saved_objects/public'; const APP_BASE_PATH: string = 'xyz/app/kibana'; +const createMockDashboardLoader = ( + dashboardToFilters: { + [dashboardId: string]: () => Filter[]; + } = {} +) => { + return { + get: async (dashboardId: string) => { + return { + searchSource: { + getField: (field: string) => { + if (field === 'filter') + return dashboardToFilters[dashboardId] ? dashboardToFilters[dashboardId]() : []; + throw new Error( + `createMockDashboardLoader > searchSource > getField > ${field} is not mocked` + ); + }, + }, + }; + }, + } as SavedObjectLoader; +}; + describe('dashboard url generator', () => { beforeEach(() => { // @ts-ignore @@ -33,7 +56,11 @@ describe('dashboard url generator', () => { test('creates a link to a saved dashboard', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({}); expect(url).toMatchInlineSnapshot(`"xyz/app/kibana#/dashboard?_a=()&_g=()"`); @@ -41,7 +68,11 @@ describe('dashboard url generator', () => { test('creates a link with global time range set up', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, @@ -53,7 +84,11 @@ describe('dashboard url generator', () => { test('creates a link with filters, time range, refresh interval and query to a saved object', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, @@ -89,7 +124,11 @@ describe('dashboard url generator', () => { test('if no useHash setting is given, uses the one was start services', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: true }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: true, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, @@ -99,7 +138,11 @@ describe('dashboard url generator', () => { test('can override a false useHash ui setting', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, @@ -110,7 +153,11 @@ describe('dashboard url generator', () => { test('can override a true useHash ui setting', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: true }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: true, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, @@ -118,4 +165,150 @@ describe('dashboard url generator', () => { }); expect(url.indexOf('relative')).toBeGreaterThan(1); }); + + describe('preserving saved filters', () => { + const savedFilter1 = { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'savedfilter1' }, + }; + + const savedFilter2 = { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'savedfilter2' }, + }; + + const appliedFilter = { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'appliedfilter' }, + }; + + test('attaches filters from destination dashboard', async () => { + const generator = createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader({ + ['dashboard1']: () => [savedFilter1], + ['dashboard2']: () => [savedFilter2], + }), + }) + ); + + const urlToDashboard1 = await generator.createUrl!({ + dashboardId: 'dashboard1', + filters: [appliedFilter], + }); + + expect(urlToDashboard1).toEqual(expect.stringContaining('query:savedfilter1')); + expect(urlToDashboard1).toEqual(expect.stringContaining('query:appliedfilter')); + + const urlToDashboard2 = await generator.createUrl!({ + dashboardId: 'dashboard2', + filters: [appliedFilter], + }); + + expect(urlToDashboard2).toEqual(expect.stringContaining('query:savedfilter2')); + expect(urlToDashboard2).toEqual(expect.stringContaining('query:appliedfilter')); + }); + + test("doesn't fail if can't retrieve filters from destination dashboard", async () => { + const generator = createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader({ + ['dashboard1']: () => { + throw new Error('Not found'); + }, + }), + }) + ); + + const url = await generator.createUrl!({ + dashboardId: 'dashboard1', + filters: [appliedFilter], + }); + + expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); + expect(url).toEqual(expect.stringContaining('query:appliedfilter')); + }); + + test('can enforce empty filters', async () => { + const generator = createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader({ + ['dashboard1']: () => [savedFilter1], + }), + }) + ); + + const url = await generator.createUrl!({ + dashboardId: 'dashboard1', + filters: [], + preserveSavedFilters: false, + }); + + expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); + expect(url).not.toEqual(expect.stringContaining('query:appliedfilter')); + expect(url).toMatchInlineSnapshot( + `"xyz/app/kibana#/dashboard/dashboard1?_a=(filters:!())&_g=(filters:!())"` + ); + }); + + test('no filters in result url if no filters applied', async () => { + const generator = createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader({ + ['dashboard1']: () => [savedFilter1], + }), + }) + ); + + const url = await generator.createUrl!({ + dashboardId: 'dashboard1', + }); + expect(url).not.toEqual(expect.stringContaining('filters')); + expect(url).toMatchInlineSnapshot(`"xyz/app/kibana#/dashboard/dashboard1?_a=()&_g=()"`); + }); + + test('can turn off preserving filters', async () => { + const generator = createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader({ + ['dashboard1']: () => [savedFilter1], + }), + }) + ); + const urlWithPreservedFiltersTurnedOff = await generator.createUrl!({ + dashboardId: 'dashboard1', + filters: [appliedFilter], + preserveSavedFilters: false, + }); + + expect(urlWithPreservedFiltersTurnedOff).not.toEqual( + expect.stringContaining('query:savedfilter1') + ); + expect(urlWithPreservedFiltersTurnedOff).toEqual( + expect.stringContaining('query:appliedfilter') + ); + }); + }); }); diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts index 0fdf395e75bca..6f121ceb2d373 100644 --- a/src/plugins/dashboard/public/url_generator.ts +++ b/src/plugins/dashboard/public/url_generator.ts @@ -27,6 +27,7 @@ import { } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; import { UrlGeneratorsDefinition, UrlGeneratorState } from '../../share/public'; +import { SavedObjectLoader } from '../../saved_objects/public'; export const STATE_STORAGE_KEY = '_a'; export const GLOBAL_STATE_STORAGE_KEY = '_g'; @@ -64,10 +65,22 @@ export type DashboardAppLinkGeneratorState = UrlGeneratorState<{ * whether to hash the data in the url to avoid url length issues. */ useHash?: boolean; + + /** + * When `true` filters from saved filters from destination dashboard as merged with applied filters + * When `false` applied filters take precedence and override saved filters + * + * true is default + */ + preserveSavedFilters?: boolean; }>; export const createDirectAccessDashboardLinkGenerator = ( - getStartServices: () => Promise<{ appBasePath: string; useHashedUrl: boolean }> + getStartServices: () => Promise<{ + appBasePath: string; + useHashedUrl: boolean; + savedDashboardLoader: SavedObjectLoader; + }> ): UrlGeneratorsDefinition => ({ id: DASHBOARD_APP_URL_GENERATOR, createUrl: async state => { @@ -76,6 +89,19 @@ export const createDirectAccessDashboardLinkGenerator = ( const appBasePath = startServices.appBasePath; const hash = state.dashboardId ? `dashboard/${state.dashboardId}` : `dashboard`; + const getSavedFiltersFromDestinationDashboardIfNeeded = async (): Promise => { + if (state.preserveSavedFilters === false) return []; + if (!state.dashboardId) return []; + try { + const dashboard = await startServices.savedDashboardLoader.get(state.dashboardId); + return dashboard?.searchSource?.getField('filter') ?? []; + } catch (e) { + // in case dashboard is missing, built the url without those filters + // dashboard app will handle redirect to landing page with toast message + return []; + } + }; + const cleanEmptyKeys = (stateObj: Record) => { Object.keys(stateObj).forEach(key => { if (stateObj[key] === undefined) { @@ -85,11 +111,18 @@ export const createDirectAccessDashboardLinkGenerator = ( return stateObj; }; + // leave filters `undefined` if no filters was applied + // in this case dashboard will restore saved filters on its own + const filters = state.filters && [ + ...(await getSavedFiltersFromDestinationDashboardIfNeeded()), + ...state.filters, + ]; + const appStateUrl = setStateToKbnUrl( STATE_STORAGE_KEY, cleanEmptyKeys({ query: state.query, - filters: state.filters?.filter(f => !esFilters.isFilterPinned(f)), + filters: filters?.filter(f => !esFilters.isFilterPinned(f)), }), { useHash }, `${appBasePath}#/${hash}` @@ -99,7 +132,7 @@ export const createDirectAccessDashboardLinkGenerator = ( GLOBAL_STATE_STORAGE_KEY, cleanEmptyKeys({ time: state.timeRange, - filters: state.filters?.filter(f => esFilters.isFilterPinned(f)), + filters: filters?.filter(f => esFilters.isFilterPinned(f)), refreshInterval: state.refreshInterval, }), { useHash }, diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts index 9829498118cc0..22ed18f75c652 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts @@ -18,14 +18,17 @@ */ import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { savedObjectsServiceMock } from '../../../../core/server/mocks'; import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; +const contextMock = savedObjectsServiceMock.createMigrationContext(); + describe('dashboard', () => { describe('7.0.0', () => { const migration = migrations['7.0.0']; test('skips error on empty object', () => { - expect(migration({} as SavedObjectUnsanitizedDoc)).toMatchInlineSnapshot(` + expect(migration({} as SavedObjectUnsanitizedDoc, contextMock)).toMatchInlineSnapshot(` Object { "references": Array [], } @@ -44,7 +47,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -83,7 +86,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -122,7 +125,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "kibanaSavedObjectMeta": Object { @@ -160,7 +163,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "kibanaSavedObjectMeta": Object { @@ -198,7 +201,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -237,7 +240,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -291,7 +294,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { @@ -331,7 +334,7 @@ Object { panelsJSON: 123, }, } as SavedObjectUnsanitizedDoc; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "panelsJSON": 123, @@ -349,7 +352,7 @@ Object { panelsJSON: '{123abc}', }, } as SavedObjectUnsanitizedDoc; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "panelsJSON": "{123abc}", @@ -367,7 +370,7 @@ Object { panelsJSON: '{}', }, } as SavedObjectUnsanitizedDoc; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "panelsJSON": "{}", @@ -385,7 +388,7 @@ Object { panelsJSON: '[{"id":"123"}]', }, } as SavedObjectUnsanitizedDoc; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "panelsJSON": "[{\\"id\\":\\"123\\"}]", @@ -403,7 +406,7 @@ Object { panelsJSON: '[{"type":"visualization"}]', }, } as SavedObjectUnsanitizedDoc; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "panelsJSON": "[{\\"type\\":\\"visualization\\"}]", @@ -422,7 +425,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, } as SavedObjectUnsanitizedDoc; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { "attributes": Object { diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index 7c1d0568cd3d7..4f7945d6dd601 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -19,7 +19,7 @@ import { get, flow } from 'lodash'; -import { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { SavedObjectMigrationFn } from 'kibana/server'; import { migrations730 } from './migrations_730'; import { migrateMatchAllQuery } from './migrate_match_all_query'; import { DashboardDoc700To720 } from '../../common'; @@ -62,7 +62,7 @@ function migrateIndexPattern(doc: DashboardDoc700To720) { doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); } -const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To720 => { +const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To720 => { // Set new "references" attribute doc.references = doc.references || []; @@ -111,7 +111,7 @@ export const dashboardSavedObjectTypeMigrations = { * in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7 * only contained the 6.7.2 migration and not the 7.0.1 migration. */ - '6.7.2': flow(migrateMatchAllQuery), - '7.0.0': flow<(doc: SavedObjectUnsanitizedDoc) => DashboardDoc700To720>(migrations700), - '7.3.0': flow(migrations730), + '6.7.2': flow>(migrateMatchAllQuery), + '7.0.0': flow>(migrations700), + '7.3.0': flow>(migrations730), }; diff --git a/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts index 5b8582bf821ef..db2fbeb278802 100644 --- a/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts +++ b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts @@ -21,7 +21,7 @@ import { SavedObjectMigrationFn } from 'kibana/server'; import { get } from 'lodash'; import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common'; -export const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { +export const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (searchSourceJSON) { diff --git a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts index aa744324428a4..a58df547fa522 100644 --- a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts @@ -17,19 +17,13 @@ * under the License. */ +import { savedObjectsServiceMock } from '../../../../core/server/mocks'; import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; import { migrations730 } from './migrations_730'; import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common'; import { RawSavedDashboardPanel730ToLatest } from '../../common'; -const mockContext = { - log: { - warning: () => {}, - warn: () => {}, - debug: () => {}, - info: () => {}, - }, -}; +const mockContext = savedObjectsServiceMock.createMigrationContext(); test('dashboard migration 7.3.0 migrates filters to query on search source', () => { const doc: DashboardDoc700To720 = { @@ -95,7 +89,7 @@ test('dashboard migration 7.3.0 migrates filters to query on search source when }, }; - const doc700: DashboardDoc700To720 = migrations['7.0.0'](doc); + const doc700 = migrations['7.0.0'](doc, mockContext); const newDoc = migrations['7.3.0'](doc700, mockContext); const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON); @@ -127,7 +121,7 @@ test('dashboard migration works when panelsJSON is missing panelIndex', () => { }, }; - const doc700: DashboardDoc700To720 = migrations['7.0.0'](doc); + const doc700 = migrations['7.0.0'](doc, mockContext); const newDoc = migrations['7.3.0'](doc700, mockContext); const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON); diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 698edbf9cd6a8..e21d27a70e02a 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -26,6 +26,7 @@ export interface IIndexPattern { id?: string; type?: string; timeFieldName?: string; + getTimeField?(): IFieldType | undefined; fieldFormatMap?: Record< string, { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 86560b3ccf7b1..91dea66f06a94 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -699,7 +699,10 @@ export function getSearchErrorType({ message }: Pick): " // Warning: (ae-missing-release-tag) "getTime" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, forceNow?: Date): import("../..").RangeFilter | undefined; +export function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, options?: { + forceNow?: Date; + fieldName?: string; +}): import("../..").RangeFilter | undefined; // Warning: (ae-missing-release-tag) "IAggConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -842,6 +845,8 @@ export interface IIndexPattern { // (undocumented) fields: IFieldType[]; // (undocumented) + getTimeField?(): IFieldType | undefined; + // (undocumented) id?: string; // (undocumented) timeFieldName?: string; diff --git a/src/plugins/data/public/query/timefilter/get_time.test.ts b/src/plugins/data/public/query/timefilter/get_time.test.ts index a8eb3a3fe8102..4dba157a6f554 100644 --- a/src/plugins/data/public/query/timefilter/get_time.test.ts +++ b/src/plugins/data/public/query/timefilter/get_time.test.ts @@ -51,5 +51,43 @@ describe('get_time', () => { }); clock.restore(); }); + + test('build range filter for non-primary field', () => { + const clock = sinon.useFakeTimers(moment.utc([2000, 1, 1, 0, 0, 0, 0]).valueOf()); + + const filter = getTime( + { + id: 'test', + title: 'test', + timeFieldName: 'date', + fields: [ + { + name: 'date', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + filterable: true, + }, + { + name: 'myCustomDate', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + filterable: true, + }, + ], + } as any, + { from: 'now-60y', to: 'now' }, + { fieldName: 'myCustomDate' } + ); + expect(filter!.range.myCustomDate).toEqual({ + gte: '1940-02-01T00:00:00.000Z', + lte: '2000-02-01T00:00:00.000Z', + format: 'strict_date_optional_time', + }); + clock.restore(); + }); }); }); diff --git a/src/plugins/data/public/query/timefilter/get_time.ts b/src/plugins/data/public/query/timefilter/get_time.ts index fa15406189041..9cdd25d3213ce 100644 --- a/src/plugins/data/public/query/timefilter/get_time.ts +++ b/src/plugins/data/public/query/timefilter/get_time.ts @@ -19,7 +19,7 @@ import dateMath from '@elastic/datemath'; import { IIndexPattern } from '../..'; -import { TimeRange, IFieldType, buildRangeFilter } from '../../../common'; +import { TimeRange, buildRangeFilter } from '../../../common'; interface CalculateBoundsOptions { forceNow?: Date; @@ -35,18 +35,27 @@ export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOp export function getTime( indexPattern: IIndexPattern | undefined, timeRange: TimeRange, + options?: { forceNow?: Date; fieldName?: string } +) { + return createTimeRangeFilter( + indexPattern, + timeRange, + options?.fieldName || indexPattern?.timeFieldName, + options?.forceNow + ); +} + +function createTimeRangeFilter( + indexPattern: IIndexPattern | undefined, + timeRange: TimeRange, + fieldName?: string, forceNow?: Date ) { if (!indexPattern) { - // in CI, we sometimes seem to fail here. return; } - - const timefield: IFieldType | undefined = indexPattern.fields.find( - field => field.name === indexPattern.timeFieldName - ); - - if (!timefield) { + const field = indexPattern.fields.find(f => f.name === (fieldName || indexPattern.timeFieldName)); + if (!field) { return; } @@ -55,7 +64,7 @@ export function getTime( return; } return buildRangeFilter( - timefield, + field, { ...(bounds.min && { gte: bounds.min.toISOString() }), ...(bounds.max && { lte: bounds.max.toISOString() }), diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts index a6260e782c12f..034af03842ab8 100644 --- a/src/plugins/data/public/query/timefilter/index.ts +++ b/src/plugins/data/public/query/timefilter/index.ts @@ -22,6 +22,6 @@ export { TimefilterService, TimefilterSetup } from './timefilter_service'; export * from './types'; export { Timefilter, TimefilterContract } from './timefilter'; export { TimeHistory, TimeHistoryContract } from './time_history'; -export { getTime } from './get_time'; +export { getTime, calculateBounds } from './get_time'; export { changeTimeFilter } from './lib/change_time_filter'; export { extractTimeFilter } from './lib/extract_time_filter'; diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts index 4fbdac47fb3b0..86ef69be572a9 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.ts @@ -164,7 +164,9 @@ export class Timefilter { }; public createFilter = (indexPattern: IndexPattern, timeRange?: TimeRange) => { - return getTime(indexPattern, timeRange ? timeRange : this._time, this.getForceNow()); + return getTime(indexPattern, timeRange ? timeRange : this._time, { + forceNow: this.getForceNow(), + }); }; public getBounds(): TimeRangeBounds { diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts index 57f3aa85ad944..3ecdc17cb57f3 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts @@ -45,7 +45,7 @@ const updateTimeBuckets = ( customBuckets?: IBucketDateHistogramAggConfig['buckets'] ) => { const bounds = - agg.params.timeRange && agg.fieldIsTimeField() + agg.params.timeRange && (agg.fieldIsTimeField() || agg.params.interval === 'auto') ? timefilter.calculateBounds(agg.params.timeRange) : undefined; const buckets = customBuckets || agg.buckets; diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 087b83127079f..eec75b0841133 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -32,8 +32,15 @@ import { Adapters } from '../../../../../plugins/inspector/public'; import { IAggConfigs } from '../aggs'; import { ISearchSource } from '../search_source'; import { tabifyAggResponse } from '../tabify'; -import { Filter, Query, serializeFieldFormat, TimeRange } from '../../../common'; -import { FilterManager, getTime } from '../../query'; +import { + Filter, + Query, + serializeFieldFormat, + TimeRange, + IIndexPattern, + isRangeFilter, +} from '../../../common'; +import { FilterManager, calculateBounds, getTime } from '../../query'; import { getSearchService, getQueryService, getIndexPatterns } from '../../services'; import { buildTabularInspectorData } from './build_tabular_inspector_data'; import { getRequestInspectorStats, getResponseInspectorStats, serializeAggConfig } from './utils'; @@ -42,6 +49,8 @@ export interface RequestHandlerParams { searchSource: ISearchSource; aggs: IAggConfigs; timeRange?: TimeRange; + timeFields?: string[]; + indexPattern?: IIndexPattern; query?: Query; filters?: Filter[]; forceFetch: boolean; @@ -65,12 +74,15 @@ interface Arguments { partialRows: boolean; includeFormatHints: boolean; aggConfigs: string; + timeFields?: string[]; } const handleCourierRequest = async ({ searchSource, aggs, timeRange, + timeFields, + indexPattern, query, filters, forceFetch, @@ -111,9 +123,19 @@ const handleCourierRequest = async ({ return aggs.onSearchRequestStart(paramSearchSource, options); }); - if (timeRange) { + // If timeFields have been specified, use the specified ones, otherwise use primary time field of index + // pattern if it's available. + const defaultTimeField = indexPattern?.getTimeField?.(); + const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : []; + const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields; + + // If a timeRange has been specified and we had at least one timeField available, create range + // filters for that those time fields + if (timeRange && allTimeFields.length > 0) { timeFilterSearchSource.setField('filter', () => { - return getTime(searchSource.getField('index'), timeRange); + return allTimeFields + .map(fieldName => getTime(indexPattern, timeRange, { fieldName })) + .filter(isRangeFilter); }); } @@ -181,11 +203,13 @@ const handleCourierRequest = async ({ (searchSource as any).finalResponse = resp; - const parsedTimeRange = timeRange ? getTime(aggs.indexPattern, timeRange) : null; + const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null; const tabifyParams = { metricsAtAllLevels, partialRows, - timeRange: parsedTimeRange ? parsedTimeRange.range : undefined, + timeRange: parsedTimeRange + ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields } + : undefined, }; const tabifyCacheHash = calculateObjectHash({ tabifyAggs: aggs, ...tabifyParams }); @@ -242,6 +266,11 @@ export const esaggs = (): ExpressionFunctionDefinition { const check = (aggResp: any, count: number, keys: string[]) => { @@ -187,9 +192,9 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 150, - lte: 350, - name: 'date', + from: moment(150), + to: moment(350), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); @@ -204,9 +209,9 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 150, - lte: 350, - name: 'date', + from: moment(150), + to: moment(350), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); @@ -221,9 +226,9 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 100, - lte: 400, - name: 'date', + from: moment(100), + to: moment(400), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); @@ -238,13 +243,47 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 150, - lte: 350, - name: 'date', + from: moment(150), + to: moment(350), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); expect(buckets).toHaveLength(4); }); + + test('does drop bucket when multiple time fields specified', () => { + const aggParams = { + drop_partials: true, + field: { + name: 'date', + }, + }; + const timeRange = { + from: moment(100), + to: moment(350), + timeFields: ['date', 'other_datefield'], + }; + const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); + + expect(buckets.buckets.map((b: Bucket) => b.key)).toEqual([100, 200]); + }); + + test('does not drop bucket when no timeFields have been specified', () => { + const aggParams = { + drop_partials: true, + field: { + name: 'date', + }, + }; + const timeRange = { + from: moment(100), + to: moment(350), + timeFields: [], + }; + const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); + + expect(buckets.buckets.map((b: Bucket) => b.key)).toEqual([0, 100, 200, 300]); + }); }); }); diff --git a/src/plugins/data/public/search/tabify/buckets.ts b/src/plugins/data/public/search/tabify/buckets.ts index 971e820ac6ddf..cd52a09caeaad 100644 --- a/src/plugins/data/public/search/tabify/buckets.ts +++ b/src/plugins/data/public/search/tabify/buckets.ts @@ -20,7 +20,7 @@ import { get, isPlainObject, keys, findKey } from 'lodash'; import moment from 'moment'; import { IAggConfig } from '../aggs'; -import { AggResponseBucket, TabbedRangeFilterParams } from './types'; +import { AggResponseBucket, TabbedRangeFilterParams, TimeRangeInformation } from './types'; type AggParams = IAggConfig['params'] & { drop_partials: boolean; @@ -36,7 +36,7 @@ export class TabifyBuckets { buckets: any; _keys: any[] = []; - constructor(aggResp: any, aggParams?: AggParams, timeRange?: TabbedRangeFilterParams) { + constructor(aggResp: any, aggParams?: AggParams, timeRange?: TimeRangeInformation) { if (aggResp && aggResp.buckets) { this.buckets = aggResp.buckets; } else if (aggResp) { @@ -107,12 +107,12 @@ export class TabifyBuckets { // dropPartials should only be called if the aggParam setting is enabled, // and the agg field is the same as the Time Range. - private dropPartials(params: AggParams, timeRange?: TabbedRangeFilterParams) { + private dropPartials(params: AggParams, timeRange?: TimeRangeInformation) { if ( !timeRange || this.buckets.length <= 1 || this.objectMode || - params.field.name !== timeRange.name + !timeRange.timeFields.includes(params.field.name) ) { return; } @@ -120,10 +120,10 @@ export class TabifyBuckets { const interval = this.buckets[1].key - this.buckets[0].key; this.buckets = this.buckets.filter((bucket: AggResponseBucket) => { - if (moment(bucket.key).isBefore(timeRange.gte)) { + if (moment(bucket.key).isBefore(timeRange.from)) { return false; } - if (moment(bucket.key + interval).isAfter(timeRange.lte)) { + if (moment(bucket.key + interval).isAfter(timeRange.to)) { return false; } return true; diff --git a/src/plugins/data/public/search/tabify/tabify.ts b/src/plugins/data/public/search/tabify/tabify.ts index e93e989034252..9cb55f94537c5 100644 --- a/src/plugins/data/public/search/tabify/tabify.ts +++ b/src/plugins/data/public/search/tabify/tabify.ts @@ -20,7 +20,7 @@ import { get } from 'lodash'; import { TabbedAggResponseWriter } from './response_writer'; import { TabifyBuckets } from './buckets'; -import { TabbedResponseWriterOptions, TabbedRangeFilterParams } from './types'; +import { TabbedResponseWriterOptions } from './types'; import { AggResponseBucket } from './types'; import { AggGroupNames, IAggConfigs } from '../aggs'; @@ -54,7 +54,7 @@ export function tabifyAggResponse( switch (agg.type.type) { case AggGroupNames.Buckets: const aggBucket = get(bucket, agg.id); - const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, timeRange); + const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, respOpts?.timeRange); if (tabifyBuckets.length) { tabifyBuckets.forEach((subBucket, tabifyBucketKey) => { @@ -153,20 +153,6 @@ export function tabifyAggResponse( doc_count: esResponse.hits.total, }; - let timeRange: TabbedRangeFilterParams | undefined; - - // Extract the time range object if provided - if (respOpts && respOpts.timeRange) { - const [timeRangeKey] = Object.keys(respOpts.timeRange); - - if (timeRangeKey) { - timeRange = { - name: timeRangeKey, - ...respOpts.timeRange[timeRangeKey], - }; - } - } - collectBucket(aggConfigs, write, topLevelBucket, '', 1); return write.response(); diff --git a/src/plugins/data/public/search/tabify/types.ts b/src/plugins/data/public/search/tabify/types.ts index 1e051880d3f19..72e91eb58c8a9 100644 --- a/src/plugins/data/public/search/tabify/types.ts +++ b/src/plugins/data/public/search/tabify/types.ts @@ -17,6 +17,7 @@ * under the License. */ +import { Moment } from 'moment'; import { RangeFilterParams } from '../../../common'; import { IAggConfig } from '../aggs'; @@ -25,11 +26,18 @@ export interface TabbedRangeFilterParams extends RangeFilterParams { name: string; } +/** @internal */ +export interface TimeRangeInformation { + from?: Moment; + to?: Moment; + timeFields: string[]; +} + /** @internal **/ export interface TabbedResponseWriterOptions { metricsAtAllLevels: boolean; partialRows: boolean; - timeRange?: { [key: string]: RangeFilterParams }; + timeRange?: TimeRangeInformation; } /** @internal */ diff --git a/src/plugins/data/server/saved_objects/index_pattern_migrations.ts b/src/plugins/data/server/saved_objects/index_pattern_migrations.ts index 7a16386ea484c..c64f7361a8cf4 100644 --- a/src/plugins/data/server/saved_objects/index_pattern_migrations.ts +++ b/src/plugins/data/server/saved_objects/index_pattern_migrations.ts @@ -20,7 +20,7 @@ import { flow, omit } from 'lodash'; import { SavedObjectMigrationFn } from 'kibana/server'; -const migrateAttributeTypeAndAttributeTypeMeta: SavedObjectMigrationFn = doc => ({ +const migrateAttributeTypeAndAttributeTypeMeta: SavedObjectMigrationFn = doc => ({ ...doc, attributes: { ...doc.attributes, @@ -29,7 +29,7 @@ const migrateAttributeTypeAndAttributeTypeMeta: SavedObjectMigrationFn = doc => }, }); -const migrateSubTypeAndParentFieldProperties: SavedObjectMigrationFn = doc => { +const migrateSubTypeAndParentFieldProperties: SavedObjectMigrationFn = doc => { if (!doc.attributes.fields) return doc; const fieldsString = doc.attributes.fields; diff --git a/src/plugins/data/server/saved_objects/search_migrations.ts b/src/plugins/data/server/saved_objects/search_migrations.ts index 45fa5e11e2a3d..c8ded51193c92 100644 --- a/src/plugins/data/server/saved_objects/search_migrations.ts +++ b/src/plugins/data/server/saved_objects/search_migrations.ts @@ -21,7 +21,7 @@ import { flow, get } from 'lodash'; import { SavedObjectMigrationFn } from 'kibana/server'; import { DEFAULT_QUERY_LANGUAGE } from '../../common'; -const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { +const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (searchSourceJSON) { @@ -55,7 +55,7 @@ const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { return doc; }; -const migrateIndexPattern: SavedObjectMigrationFn = doc => { +const migrateIndexPattern: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (typeof searchSourceJSON !== 'string') { return doc; @@ -97,13 +97,13 @@ const migrateIndexPattern: SavedObjectMigrationFn = doc => { return doc; }; -const setNewReferences: SavedObjectMigrationFn = (doc, context) => { +const setNewReferences: SavedObjectMigrationFn = (doc, context) => { doc.references = doc.references || []; // Migrate index pattern return migrateIndexPattern(doc, context); }; -const migrateSearchSortToNestedArray: SavedObjectMigrationFn = doc => { +const migrateSearchSortToNestedArray: SavedObjectMigrationFn = doc => { const sort = get(doc, 'attributes.sort'); if (!sort) return doc; @@ -122,7 +122,7 @@ const migrateSearchSortToNestedArray: SavedObjectMigrationFn = doc => { }; export const searchSavedObjectTypeMigrations = { - '6.7.2': flow(migrateMatchAllQuery), - '7.0.0': flow(setNewReferences), - '7.4.0': flow(migrateSearchSortToNestedArray), + '6.7.2': flow>(migrateMatchAllQuery), + '7.0.0': flow>(setNewReferences), + '7.4.0': flow>(migrateSearchSortToNestedArray), }; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 5d94b6516c2ba..df4ba23244b4d 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -408,6 +408,8 @@ export interface IIndexPattern { // (undocumented) fields: IFieldType[]; // (undocumented) + getTimeField?(): IFieldType | undefined; + // (undocumented) id?: string; // (undocumented) timeFieldName?: string; diff --git a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx index fe3150fc0bb07..c1daf3445219f 100644 --- a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx +++ b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx @@ -17,23 +17,15 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React, { lazy, Suspense } from 'react'; import ReactDOM from 'react-dom'; -import { HashRouter, Switch, Route, useParams, useLocation } from 'react-router-dom'; -import { parse } from 'query-string'; -import { get } from 'lodash'; -import { i18n } from '@kbn/i18n'; +import { HashRouter, Switch, Route } from 'react-router-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreSetup, CoreStart, ChromeBreadcrumb, Capabilities } from 'src/core/public'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { CoreSetup, Capabilities } from 'src/core/public'; import { ManagementAppMountParams } from '../../../management/public'; -import { DataPublicPluginStart } from '../../../data/public'; import { StartDependencies, SavedObjectsManagementPluginStart } from '../plugin'; -import { - ISavedObjectsManagementServiceRegistry, - SavedObjectsManagementActionServiceStart, -} from '../services'; -import { SavedObjectsTable } from './objects_table'; -import { SavedObjectEdition } from './object_view'; +import { ISavedObjectsManagementServiceRegistry } from '../services'; import { getAllowedTypes } from './../lib'; interface MountParams { @@ -44,6 +36,8 @@ interface MountParams { let allowedObjectTypes: string[] | undefined; +const SavedObjectsEditionPage = lazy(() => import('./saved_objects_edition_page')); +const SavedObjectsTablePage = lazy(() => import('./saved_objects_table_page')); export const mountManagementSection = async ({ core, mountParams, @@ -63,23 +57,27 @@ export const mountManagementSection = async ({ - + }> + + - + }> + + @@ -103,110 +101,3 @@ const RedirectToHomeIfUnauthorized: React.FunctionComponent<{ } return children! as React.ReactElement; }; - -const SavedObjectsEditionPage = ({ - coreStart, - serviceRegistry, - setBreadcrumbs, -}: { - coreStart: CoreStart; - serviceRegistry: ISavedObjectsManagementServiceRegistry; - setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; -}) => { - const { service: serviceName, id } = useParams<{ service: string; id: string }>(); - const capabilities = coreStart.application.capabilities; - - const { search } = useLocation(); - const query = parse(search); - const service = serviceRegistry.get(serviceName); - - useEffect(() => { - setBreadcrumbs([ - { - text: i18n.translate('savedObjectsManagement.breadcrumb.index', { - defaultMessage: 'Saved objects', - }), - href: '#/management/kibana/objects', - }, - { - text: i18n.translate('savedObjectsManagement.breadcrumb.edit', { - defaultMessage: 'Edit {savedObjectType}', - values: { savedObjectType: service?.service.type ?? 'object' }, - }), - }, - ]); - }, [setBreadcrumbs, service]); - - return ( - - ); -}; - -const SavedObjectsTablePage = ({ - coreStart, - dataStart, - allowedTypes, - serviceRegistry, - actionRegistry, - setBreadcrumbs, -}: { - coreStart: CoreStart; - dataStart: DataPublicPluginStart; - allowedTypes: string[]; - serviceRegistry: ISavedObjectsManagementServiceRegistry; - actionRegistry: SavedObjectsManagementActionServiceStart; - setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; -}) => { - const capabilities = coreStart.application.capabilities; - const itemsPerPage = coreStart.uiSettings.get('savedObjects:perPage', 50); - - useEffect(() => { - setBreadcrumbs([ - { - text: i18n.translate('savedObjectsManagement.breadcrumb.index', { - defaultMessage: 'Saved objects', - }), - href: '#/management/kibana/objects', - }, - ]); - }, [setBreadcrumbs]); - - return ( - { - const { editUrl } = savedObject.meta; - if (editUrl) { - // previously, kbnUrl.change(object.meta.editUrl); was used. - // using direct access to location.hash seems the only option for now, - // as using react-router-dom will prefix the url with the router's basename - // which should be ignored there. - window.location.hash = editUrl; - } - }} - canGoInApp={savedObject => { - const { inAppUrl } = savedObject.meta; - return inAppUrl ? get(capabilities, inAppUrl.uiCapabilitiesPath) : false; - }} - /> - ); -}; diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx new file mode 100644 index 0000000000000..5ac6e8e103c47 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect } from 'react'; +import { useParams, useLocation } from 'react-router-dom'; +import { parse } from 'query-string'; +import { i18n } from '@kbn/i18n'; +import { CoreStart, ChromeBreadcrumb } from 'src/core/public'; +import { ISavedObjectsManagementServiceRegistry } from '../services'; +import { SavedObjectEdition } from './object_view'; + +const SavedObjectsEditionPage = ({ + coreStart, + serviceRegistry, + setBreadcrumbs, +}: { + coreStart: CoreStart; + serviceRegistry: ISavedObjectsManagementServiceRegistry; + setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; +}) => { + const { service: serviceName, id } = useParams<{ service: string; id: string }>(); + const capabilities = coreStart.application.capabilities; + + const { search } = useLocation(); + const query = parse(search); + const service = serviceRegistry.get(serviceName); + + useEffect(() => { + setBreadcrumbs([ + { + text: i18n.translate('savedObjectsManagement.breadcrumb.index', { + defaultMessage: 'Saved objects', + }), + href: '#/management/kibana/objects', + }, + { + text: i18n.translate('savedObjectsManagement.breadcrumb.edit', { + defaultMessage: 'Edit {savedObjectType}', + values: { savedObjectType: service?.service.type ?? 'object' }, + }), + }, + ]); + }, [setBreadcrumbs, service]); + + return ( + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SavedObjectsEditionPage as default }; diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx new file mode 100644 index 0000000000000..7660d17f91c5b --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect } from 'react'; +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { CoreStart, ChromeBreadcrumb } from 'src/core/public'; +import { DataPublicPluginStart } from '../../../data/public'; +import { + ISavedObjectsManagementServiceRegistry, + SavedObjectsManagementActionServiceStart, +} from '../services'; +import { SavedObjectsTable } from './objects_table'; + +const SavedObjectsTablePage = ({ + coreStart, + dataStart, + allowedTypes, + serviceRegistry, + actionRegistry, + setBreadcrumbs, +}: { + coreStart: CoreStart; + dataStart: DataPublicPluginStart; + allowedTypes: string[]; + serviceRegistry: ISavedObjectsManagementServiceRegistry; + actionRegistry: SavedObjectsManagementActionServiceStart; + setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; +}) => { + const capabilities = coreStart.application.capabilities; + const itemsPerPage = coreStart.uiSettings.get('savedObjects:perPage', 50); + + useEffect(() => { + setBreadcrumbs([ + { + text: i18n.translate('savedObjectsManagement.breadcrumb.index', { + defaultMessage: 'Saved objects', + }), + href: '#/management/kibana/objects', + }, + ]); + }, [setBreadcrumbs]); + + return ( + { + const { editUrl } = savedObject.meta; + if (editUrl) { + // previously, kbnUrl.change(object.meta.editUrl); was used. + // using direct access to location.hash seems the only option for now, + // as using react-router-dom will prefix the url with the router's basename + // which should be ignored there. + window.location.hash = editUrl; + } + }} + canGoInApp={savedObject => { + const { inAppUrl } = savedObject.meta; + return inAppUrl ? get(capabilities, inAppUrl.uiCapabilitiesPath) : false; + }} + /> + ); +}; +// eslint-disable-next-line import/no-default-export +export { SavedObjectsTablePage as default }; diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap new file mode 100644 index 0000000000000..8c0117e5a7266 --- /dev/null +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -0,0 +1,492 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TelemetryManagementSectionComponent renders as expected 1`] = ` + + + + + + +

+ +

+
+
+
+ + + + , + } + } + /> +

+ } + /> + + +

+ + + , + } + } + /> +

+

+ + + +

+ , + "displayName": "Provide usage statistics", + "name": "telemetry:enabled", + "type": "boolean", + "value": true, + } + } + toasts={null} + /> +
+
+
+`; + +exports[`TelemetryManagementSectionComponent renders null because allowChangingOptInStatus is false 1`] = ` + +`; + +exports[`TelemetryManagementSectionComponent renders null because query does not match the SEARCH_TERMS 1`] = ` + +`; + +exports[`TelemetryManagementSectionComponent test the wrapper (for coverage purposes) 1`] = `""`; diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx new file mode 100644 index 0000000000000..d0c2bd13f802d --- /dev/null +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx @@ -0,0 +1,284 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { TelemetryManagementSection } from './telemetry_management_section'; +import { TelemetryService } from '../../../telemetry/public/services'; +import { coreMock } from '../../../../core/public/mocks'; +import { telemetryManagementSectionWrapper } from './telemetry_management_section_wrapper'; + +describe('TelemetryManagementSectionComponent', () => { + const coreStart = coreMock.createStart(); + const coreSetup = coreMock.createSetup(); + + it('renders as expected', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: true, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + expect( + shallowWithIntl( + + ) + ).toMatchSnapshot(); + }); + + it('renders null because query does not match the SEARCH_TERMS', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + expect( + component.setProps({ ...component.props(), query: { text: 'asssdasdsad' } }) + ).toMatchSnapshot(); + expect(onQueryMatchChange).toHaveBeenCalledWith(false); + expect(onQueryMatchChange).toHaveBeenCalledTimes(1); + } finally { + component.unmount(); + } + }); + + it('renders because query matches the SEARCH_TERMS', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + expect( + component.setProps({ ...component.props(), query: { text: 'TeLEMetry' } }).html() + ).not.toBe(''); // Renders something. + // I can't check against snapshot because of https://github.com/facebook/jest/issues/8618 + // expect(component).toMatchSnapshot(); + + // It should also render if there is no query at all. + expect(component.setProps({ ...component.props(), query: {} }).html()).not.toBe(''); + expect(onQueryMatchChange).toHaveBeenCalledWith(true); + + // Should only be called once because the second time does not change the result + expect(onQueryMatchChange).toHaveBeenCalledTimes(1); + } finally { + component.unmount(); + } + }); + + it('renders null because allowChangingOptInStatus is false', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: false, + optIn: true, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + expect(component).toMatchSnapshot(); + component.setProps({ ...component.props(), query: { text: 'TeLEMetry' } }); + expect(onQueryMatchChange).toHaveBeenCalledWith(false); + } finally { + component.unmount(); + } + }); + + it('shows the OptInExampleFlyout', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + const toggleExampleComponent = component.find('p > EuiLink[onClick]'); + const updatedView = toggleExampleComponent.simulate('click'); + updatedView.find('OptInExampleFlyout'); + updatedView.simulate('close'); + } finally { + component.unmount(); + } + }); + + it('toggles the OptIn button', async () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + const toggleOptInComponent = component.find('Field'); + await expect( + toggleOptInComponent.prop('handleChange')() + ).resolves.toBe(true); + expect((component.state() as any).enabled).toBe(true); + await expect( + toggleOptInComponent.prop('handleChange')() + ).resolves.toBe(true); + expect((component.state() as any).enabled).toBe(false); + telemetryService.setOptIn = jest.fn().mockRejectedValue(Error('test-error')); + await expect( + toggleOptInComponent.prop('handleChange')() + ).rejects.toStrictEqual(Error('test-error')); + } finally { + component.unmount(); + } + }); + + it('test the wrapper (for coverage purposes)', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: false, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + const Wrapper = telemetryManagementSectionWrapper(telemetryService); + expect( + shallowWithIntl( + + ).html() + ).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx index 26e075b666593..361c5ff719c54 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx @@ -69,7 +69,9 @@ export class TelemetryManagementSection extends Component { const { query } = nextProps; const searchTerm = (query.text || '').toLowerCase(); - const searchTermMatches = SEARCH_TERMS.some(term => term.indexOf(searchTerm) >= 0); + const searchTermMatches = + this.props.telemetryService.getCanChangeOptInStatus() && + SEARCH_TERMS.some(term => term.indexOf(searchTerm) >= 0); if (searchTermMatches !== this.state.queryMatches) { this.setState( diff --git a/src/plugins/vis_type_markdown/public/markdown_vis.ts b/src/plugins/vis_type_markdown/public/markdown_vis.ts index b84d9638eb973..3309330d7527c 100644 --- a/src/plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/plugins/vis_type_markdown/public/markdown_vis.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { MarkdownVisWrapper } from './markdown_vis_controller'; import { MarkdownOptions } from './markdown_options'; -import { SettingsOptions } from './settings_options'; +import { SettingsOptions } from './settings_options_lazy'; import { DefaultEditorSize } from '../../vis_default_editor/public'; export const markdownVisDefinition = { diff --git a/src/plugins/vis_type_markdown/public/settings_options.tsx b/src/plugins/vis_type_markdown/public/settings_options.tsx index 6f6a80564ce07..bf4570db5d4a0 100644 --- a/src/plugins/vis_type_markdown/public/settings_options.tsx +++ b/src/plugins/vis_type_markdown/public/settings_options.tsx @@ -52,4 +52,6 @@ function SettingsOptions({ stateParams, setValue }: VisOptionsProps import('./settings_options')); + +export const SettingsOptions = (props: any) => ( + }> + + +); diff --git a/src/plugins/vis_type_table/public/get_inner_angular.ts b/src/plugins/vis_type_table/public/get_inner_angular.ts index d69b9bba31b03..e8404f918d609 100644 --- a/src/plugins/vis_type_table/public/get_inner_angular.ts +++ b/src/plugins/vis_type_table/public/get_inner_angular.ts @@ -21,6 +21,8 @@ // these are necessary to bootstrap the local angular. // They can stay even after NP cutover import angular from 'angular'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import 'angular-recursion'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { CoreStart, IUiSettingsClient, PluginInitializerContext } from 'kibana/public'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js index 7075e86eb56bf..9fdb8ccc919b7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js @@ -240,3 +240,7 @@ VisEditor.propTypes = { timeRange: PropTypes.object, appState: PropTypes.object, }; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { VisEditor as default }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx new file mode 100644 index 0000000000000..d81bd95d8d771 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +// @ts-ignore +const VisEditorComponent = lazy(() => import('./vis_editor')); + +export const VisEditor = (props: any) => ( + }> + + +); diff --git a/src/plugins/vis_type_timeseries/public/application/editor_controller.js b/src/plugins/vis_type_timeseries/public/application/editor_controller.js index af50d3a06d1fc..f21b5f947bca7 100644 --- a/src/plugins/vis_type_timeseries/public/application/editor_controller.js +++ b/src/plugins/vis_type_timeseries/public/application/editor_controller.js @@ -21,7 +21,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { fetchIndexPatternFields } from './lib/fetch_fields'; import { getSavedObjectsClient, getUISettings, getI18n } from '../services'; -import { VisEditor } from './components/vis_editor'; +import { VisEditor } from './components/vis_editor_lazy'; export class EditorController { constructor(el, vis, eventEmitter, embeddableHandler) { diff --git a/src/plugins/vis_type_timeseries/public/metrics_fn.ts b/src/plugins/vis_type_timeseries/public/metrics_fn.ts index 008b13cce6565..b573225feaab1 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_fn.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_fn.ts @@ -20,7 +20,6 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition, KibanaContext, Render } from '../../expressions/public'; -import { PersistedState } from '../../visualizations/public'; // @ts-ignore import { metricsRequestHandler } from './request_handler'; @@ -76,6 +75,7 @@ export const createMetricsFn = (): ExpressionFunctionDefinition< const params = JSON.parse(args.params); const uiStateParams = JSON.parse(args.uiState); const savedObjectId = args.savedObjectId; + const { PersistedState } = await import('../../visualizations/public'); const uiState = new PersistedState(uiStateParams); const response = await metricsRequestHandler({ diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index c525ce7fa0b3b..2b0734ceb4d4d 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -25,6 +25,7 @@ import { EditorController } from './application'; // @ts-ignore import { PANEL_TYPES } from '../common/panel_types'; import { defaultFeedbackMessage } from '../../kibana_utils/public'; +import { VisEditor } from './application/components/vis_editor_lazy'; export const metricsVisDefinition = { name: 'metrics', @@ -69,7 +70,7 @@ export const metricsVisDefinition = { show_legend: 1, show_grid: 1, }, - component: require('./application/components/vis_editor').VisEditor, + component: VisEditor, }, editor: EditorController, options: { diff --git a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts index 34922976f22ff..1e5508b44ee0e 100644 --- a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts +++ b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts @@ -20,7 +20,7 @@ import { flow } from 'lodash'; import { SavedObjectMigrationFn, SavedObjectsType } from 'kibana/server'; -const resetCount: SavedObjectMigrationFn = doc => ({ +const resetCount: SavedObjectMigrationFn = doc => ({ ...doc, attributes: { ...doc.attributes, diff --git a/src/plugins/vis_type_vega/public/vega_request_handler.ts b/src/plugins/vis_type_vega/public/vega_request_handler.ts index 196e8fdcbafda..efc02e368efa8 100644 --- a/src/plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/plugins/vis_type_vega/public/vega_request_handler.ts @@ -19,8 +19,6 @@ import { Filter, esQuery, TimeRange, Query } from '../../data/public'; -// @ts-ignore -import { VegaParser } from './data_model/vega_parser'; // @ts-ignore import { SearchCache } from './data_model/search_cache'; // @ts-ignore @@ -46,7 +44,12 @@ export function createVegaRequestHandler({ const { timefilter } = data.query.timefilter; const timeCache = new TimeCache(timefilter, 3 * 1000); - return ({ timeRange, filters, query, visParams }: VegaRequestHandlerParams) => { + return async function vegaRequestHandler({ + timeRange, + filters, + query, + visParams, + }: VegaRequestHandlerParams) { if (!searchCache) { searchCache = new SearchCache(getData().search.__LEGACY.esClient, { max: 10, @@ -58,8 +61,10 @@ export function createVegaRequestHandler({ const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); const filtersDsl = esQuery.buildEsQuery(undefined, query, filters, esQueryConfigs); + // @ts-ignore + const { VegaParser } = await import('./data_model/vega_parser'); const vp = new VegaParser(visParams.spec, searchCache, timeCache, filtersDsl, serviceSettings); - return vp.parseAsync(); + return await vp.parseAsync(); }; } diff --git a/src/plugins/vis_type_vega/public/vega_visualization.js b/src/plugins/vis_type_vega/public/vega_visualization.js index a6e911de7f0cb..1fcb89f04457d 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.js @@ -17,8 +17,6 @@ * under the License. */ import { i18n } from '@kbn/i18n'; -import { VegaView } from './vega_view/vega_view'; -import { VegaMapView } from './vega_view/vega_map_view'; import { getNotifications, getData, getSavedObjects } from './services'; export const createVegaVisualization = ({ serviceSettings }) => @@ -117,8 +115,10 @@ export const createVegaVisualization = ({ serviceSettings }) => if (vegaParser.useMap) { const services = { toastService: getNotifications().toasts }; + const { VegaMapView } = await import('./vega_view/vega_map_view'); this._vegaView = new VegaMapView(vegaViewParams, services); } else { + const { VegaView } = await import('./vega_view/vega_view'); this._vegaView = new VegaView(vegaViewParams); } await this._vegaView.init(); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts index 94473e35a942d..f6455d0c1e43f 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts @@ -21,7 +21,7 @@ import { SavedObjectMigrationFn } from 'kibana/server'; import { cloneDeep, get, omit, has, flow } from 'lodash'; import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common'; -const migrateIndexPattern: SavedObjectMigrationFn = doc => { +const migrateIndexPattern: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (typeof searchSourceJSON !== 'string') { return doc; @@ -64,7 +64,7 @@ const migrateIndexPattern: SavedObjectMigrationFn = doc => { }; // [TSVB] Migrate percentile-rank aggregation (value -> values) -const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => { +const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); let visState; @@ -100,7 +100,7 @@ const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => { }; // [TSVB] Remove stale opperator key -const migrateOperatorKeyTypo: SavedObjectMigrationFn = doc => { +const migrateOperatorKeyTypo: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); let visState; @@ -132,7 +132,7 @@ const migrateOperatorKeyTypo: SavedObjectMigrationFn = doc => { }; // Migrate date histogram aggregation (remove customInterval) -const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => { +const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); let visState; @@ -174,7 +174,7 @@ const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => { return doc; }; -const removeDateHistogramTimeZones: SavedObjectMigrationFn = doc => { +const removeDateHistogramTimeZones: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { let visState; @@ -206,7 +206,7 @@ const removeDateHistogramTimeZones: SavedObjectMigrationFn = doc => { // migrate gauge verticalSplit to alignment // https://github.com/elastic/kibana/issues/34636 -const migrateGaugeVerticalSplitToAlignment: SavedObjectMigrationFn = (doc, logger) => { +const migrateGaugeVerticalSplitToAlignment: SavedObjectMigrationFn = (doc, logger) => { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { @@ -241,7 +241,7 @@ const migrateGaugeVerticalSplitToAlignment: SavedObjectMigrationFn = (doc, logge Path to the series array is thus: attributes.visState. */ -const transformFilterStringToQueryObject: SavedObjectMigrationFn = (doc, logger) => { +const transformFilterStringToQueryObject: SavedObjectMigrationFn = (doc, logger) => { // Migrate filters // If any filters exist and they are a string, we assume it to be lucene and transform the filter into an object accordingly const newDoc = cloneDeep(doc); @@ -325,7 +325,7 @@ const transformFilterStringToQueryObject: SavedObjectMigrationFn = (doc, logger) return newDoc; }; -const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn = doc => { +const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn = doc => { // Migrate split_filters in TSVB objects that weren't migrated in 7.3 // If any filters exist and they are a string, we assume them to be lucene syntax and transform the filter into an object accordingly const newDoc = cloneDeep(doc); @@ -370,7 +370,7 @@ const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn = doc => return newDoc; }; -const migrateFiltersAggQuery: SavedObjectMigrationFn = doc => { +const migrateFiltersAggQuery: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { @@ -402,7 +402,7 @@ const migrateFiltersAggQuery: SavedObjectMigrationFn = doc => { return doc; }; -const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => { +const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => { const visStateJSON = get(doc, 'attributes.visState'); let visState; @@ -450,7 +450,7 @@ const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => { return doc; }; -const migrateFiltersAggQueryStringQueries: SavedObjectMigrationFn = (doc, logger) => { +const migrateFiltersAggQueryStringQueries: SavedObjectMigrationFn = (doc, logger) => { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { @@ -483,12 +483,12 @@ const migrateFiltersAggQueryStringQueries: SavedObjectMigrationFn = (doc, logger return doc; }; -const addDocReferences: SavedObjectMigrationFn = doc => ({ +const addDocReferences: SavedObjectMigrationFn = doc => ({ ...doc, references: doc.references || [], }); -const migrateSavedSearch: SavedObjectMigrationFn = doc => { +const migrateSavedSearch: SavedObjectMigrationFn = doc => { const savedSearchId = get(doc, 'attributes.savedSearchId'); if (savedSearchId && doc.references) { @@ -505,7 +505,7 @@ const migrateSavedSearch: SavedObjectMigrationFn = doc => { return doc; }; -const migrateControls: SavedObjectMigrationFn = doc => { +const migrateControls: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { @@ -536,7 +536,7 @@ const migrateControls: SavedObjectMigrationFn = doc => { return doc; }; -const migrateTableSplits: SavedObjectMigrationFn = doc => { +const migrateTableSplits: SavedObjectMigrationFn = doc => { try { const visState = JSON.parse(doc.attributes.visState); if (get(visState, 'type') !== 'table') { @@ -572,7 +572,7 @@ const migrateTableSplits: SavedObjectMigrationFn = doc => { } }; -const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { +const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (searchSourceJSON) { @@ -606,7 +606,7 @@ const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { }; // [TSVB] Default color palette is changing, keep the default for older viz -const migrateTsvbDefaultColorPalettes: SavedObjectMigrationFn = doc => { +const migrateTsvbDefaultColorPalettes: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); let visState; @@ -649,27 +649,30 @@ export const visualizationSavedObjectTypeMigrations = { * in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7 * only contained the 6.7.2 migration and not the 7.0.1 migration. */ - '6.7.2': flow(migrateMatchAllQuery, removeDateHistogramTimeZones), - '7.0.0': flow( + '6.7.2': flow>( + migrateMatchAllQuery, + removeDateHistogramTimeZones + ), + '7.0.0': flow>( addDocReferences, migrateIndexPattern, migrateSavedSearch, migrateControls, migrateTableSplits ), - '7.0.1': flow(removeDateHistogramTimeZones), - '7.2.0': flow( + '7.0.1': flow>(removeDateHistogramTimeZones), + '7.2.0': flow>( migratePercentileRankAggregation, migrateDateHistogramAggregation ), - '7.3.0': flow( + '7.3.0': flow>( migrateGaugeVerticalSplitToAlignment, transformFilterStringToQueryObject, migrateFiltersAggQuery, replaceMovAvgToMovFn ), - '7.3.1': flow(migrateFiltersAggQueryStringQueries), - '7.4.2': flow(transformSplitFiltersStringToQueryObject), - '7.7.0': flow(migrateOperatorKeyTypo), - '7.8.0': flow(migrateTsvbDefaultColorPalettes), + '7.3.1': flow>(migrateFiltersAggQueryStringQueries), + '7.4.2': flow>(transformSplitFiltersStringToQueryObject), + '7.7.0': flow>(migrateOperatorKeyTypo), + '7.8.0': flow>(migrateTsvbDefaultColorPalettes), }; diff --git a/src/plugins/visualize/public/application/application.ts b/src/plugins/visualize/public/application/application.ts index 9d8a1b98ef023..19551bba9a43e 100644 --- a/src/plugins/visualize/public/application/application.ts +++ b/src/plugins/visualize/public/application/application.ts @@ -20,6 +20,8 @@ import './index.scss'; import angular, { IModule } from 'angular'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext } from 'kibana/public'; diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 1ec7c831b4864..f87edbf04f220 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -25,6 +25,7 @@ import { DllCompiler } from '../../src/optimize/dynamic_dll_plugin'; const TOTAL_CI_SHARDS = 4; const ROOT = dirname(require.resolve('../../package.json')); +const buildHash = String(Number.MAX_SAFE_INTEGER); module.exports = function(grunt) { function pickBrowser() { @@ -57,27 +58,30 @@ module.exports = function(grunt) { 'http://localhost:5610/test_bundle/karma/globals.js', ...UiSharedDeps.jsDepFilenames.map( - chunkFilename => `http://localhost:5610/bundles/kbn-ui-shared-deps/${chunkFilename}` + chunkFilename => + `http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${chunkFilename}` ), - `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, + `http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, - 'http://localhost:5610/built_assets/dlls/vendors_runtime.bundle.dll.js', + `http://localhost:5610/${buildHash}/built_assets/dlls/vendors_runtime.bundle.dll.js`, ...DllCompiler.getRawDllConfig().chunks.map( - chunk => `http://localhost:5610/built_assets/dlls/vendors${chunk}.bundle.dll.js` + chunk => + `http://localhost:5610/${buildHash}/built_assets/dlls/vendors${chunk}.bundle.dll.js` ), shardNum === undefined - ? `http://localhost:5610/bundles/tests.bundle.js` - : `http://localhost:5610/bundles/tests.bundle.js?shards=${TOTAL_CI_SHARDS}&shard_num=${shardNum}`, + ? `http://localhost:5610/${buildHash}/bundles/tests.bundle.js` + : `http://localhost:5610/${buildHash}/bundles/tests.bundle.js?shards=${TOTAL_CI_SHARDS}&shard_num=${shardNum}`, - `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, + `http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, // this causes tilemap tests to fail, probably because the eui styles haven't been // included in the karma harness a long some time, if ever // `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`, ...DllCompiler.getRawDllConfig().chunks.map( - chunk => `http://localhost:5610/built_assets/dlls/vendors${chunk}.style.dll.css` + chunk => + `http://localhost:5610/${buildHash}/built_assets/dlls/vendors${chunk}.style.dll.css` ), - 'http://localhost:5610/bundles/tests.style.css', + `http://localhost:5610/${buildHash}/bundles/tests.style.css`, ]; } @@ -127,9 +131,9 @@ module.exports = function(grunt) { proxies: { '/tests/': 'http://localhost:5610/tests/', - '/bundles/': 'http://localhost:5610/bundles/', - '/built_assets/dlls/': 'http://localhost:5610/built_assets/dlls/', '/test_bundle/': 'http://localhost:5610/test_bundle/', + [`/${buildHash}/bundles/`]: `http://localhost:5610/${buildHash}/bundles/`, + [`/${buildHash}/built_assets/dlls/`]: `http://localhost:5610/${buildHash}/built_assets/dlls/`, }, client: { diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index c5bfc847d0041..0c4028905657d 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -33,5 +33,6 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./status')); loadTestFile(require.resolve('./stats')); loadTestFile(require.resolve('./ui_metric')); + loadTestFile(require.resolve('./telemetry')); }); } diff --git a/src/core/public/plugins/plugin_loader.mock.ts b/test/api_integration/apis/telemetry/index.js similarity index 57% rename from src/core/public/plugins/plugin_loader.mock.ts rename to test/api_integration/apis/telemetry/index.js index abdd9d4ddce2a..c79f5cb470890 100644 --- a/src/core/public/plugins/plugin_loader.mock.ts +++ b/test/api_integration/apis/telemetry/index.js @@ -17,17 +17,10 @@ * under the License. */ -import { PluginName } from 'src/core/server'; -import { LoadPluginBundle, UnknownPluginInitializer } from './plugin_loader'; - -/** - * @param initializerProvider A function provided by the test to resolve initializers. - */ -const createLoadPluginBundleMock = ( - initializerProvider: (name: PluginName) => UnknownPluginInitializer -): jest.Mock, Parameters> => - jest.fn((addBasePath, pluginName, _ = {}) => { - return Promise.resolve(initializerProvider(pluginName)) as any; +export default function({ loadTestFile }) { + describe('Telemetry', () => { + loadTestFile(require.resolve('./telemetry_local')); + loadTestFile(require.resolve('./opt_in')); + loadTestFile(require.resolve('./telemetry_optin_notice_seen')); }); - -export const loadPluginBundleMock = { create: createLoadPluginBundleMock }; +} diff --git a/test/api_integration/apis/telemetry/opt_in.ts b/test/api_integration/apis/telemetry/opt_in.ts new file mode 100644 index 0000000000000..e4654ee3985f3 --- /dev/null +++ b/test/api_integration/apis/telemetry/opt_in.ts @@ -0,0 +1,123 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; + +import { TelemetrySavedObjectAttributes } from 'src/plugins/telemetry/server/telemetry_repository'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function optInTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + describe('/api/telemetry/v2/optIn API', () => { + let defaultAttributes: TelemetrySavedObjectAttributes; + let kibanaVersion: any; + before(async () => { + const kibanaVersionAccessor = kibanaServer.version; + kibanaVersion = await kibanaVersionAccessor.get(); + defaultAttributes = + (await getSavedObjectAttributes(supertest).catch(err => { + if (err.message === 'expected 200 "OK", got 404 "Not Found"') { + return null; + } + throw err; + })) || {}; + + expect(typeof kibanaVersion).to.eql('string'); + expect(kibanaVersion.length).to.be.greaterThan(0); + }); + + afterEach(async () => { + await updateSavedObjectAttributes(supertest, defaultAttributes); + }); + + it('should support sending false with allowChangingOptInStatus true', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: true, + }); + await postTelemetryV2Optin(supertest, false, 200); + const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest); + expect(enabled).to.be(false); + expect(lastVersionChecked).to.be(kibanaVersion); + }); + + it('should support sending true with allowChangingOptInStatus true', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: true, + }); + await postTelemetryV2Optin(supertest, true, 200); + const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest); + expect(enabled).to.be(true); + expect(lastVersionChecked).to.be(kibanaVersion); + }); + + it('should not support sending false with allowChangingOptInStatus false', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: false, + }); + await postTelemetryV2Optin(supertest, false, 400); + }); + + it('should not support sending true with allowChangingOptInStatus false', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: false, + }); + await postTelemetryV2Optin(supertest, true, 400); + }); + + it('should not support sending null', async () => { + await postTelemetryV2Optin(supertest, null, 400); + }); + + it('should not support sending junk', async () => { + await postTelemetryV2Optin(supertest, 42, 400); + }); + }); +} + +async function postTelemetryV2Optin(supertest: any, value: any, statusCode: number): Promise { + const { body } = await supertest + .post('/api/telemetry/v2/optIn') + .set('kbn-xsrf', 'xxx') + .send({ enabled: value }) + .expect(statusCode); + + return body; +} + +async function updateSavedObjectAttributes( + supertest: any, + attributes: TelemetrySavedObjectAttributes +): Promise { + return await supertest + .post('/api/saved_objects/telemetry/telemetry') + .query({ overwrite: true }) + .set('kbn-xsrf', 'xxx') + .send({ attributes }) + .expect(200); +} + +async function getSavedObjectAttributes(supertest: any): Promise { + const { body } = await supertest.get('/api/saved_objects/telemetry/telemetry').expect(200); + return body.attributes; +} diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js new file mode 100644 index 0000000000000..84bfd8a755c11 --- /dev/null +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -0,0 +1,133 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import _ from 'lodash'; + +/* + * Create a single-level array with strings for all the paths to values in the + * source object, up to 3 deep. Going deeper than 3 causes a bit too much churn + * in the tests. + */ +function flatKeys(source) { + const recursivelyFlatKeys = (obj, path = [], depth = 0) => { + return depth < 3 && _.isObject(obj) + ? _.map(obj, (v, k) => recursivelyFlatKeys(v, [...path, k], depth + 1)) + : path.join('.'); + }; + + return _.uniq(_.flattenDeep(recursivelyFlatKeys(source))).sort((a, b) => a.localeCompare(b)); +} + +export default function({ getService }) { + const supertest = getService('supertest'); + + describe('/api/telemetry/v2/clusters/_stats', () => { + it('should pull local stats and validate data types', async () => { + const timeRange = { + min: '2018-07-23T22:07:00Z', + max: '2018-07-23T22:13:00Z', + }; + + const { body } = await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ timeRange, unencrypted: true }) + .expect(200); + + expect(body.length).to.be(1); + const stats = body[0]; + expect(stats.collection).to.be('local'); + expect(stats.stack_stats.kibana.count).to.be.a('number'); + expect(stats.stack_stats.kibana.indices).to.be.a('number'); + expect(stats.stack_stats.kibana.os.platforms[0].platform).to.be.a('string'); + expect(stats.stack_stats.kibana.os.platforms[0].count).to.be(1); + expect(stats.stack_stats.kibana.os.platformReleases[0].platformRelease).to.be.a('string'); + expect(stats.stack_stats.kibana.os.platformReleases[0].count).to.be(1); + expect(stats.stack_stats.kibana.plugins.telemetry.opt_in_status).to.be(false); + expect(stats.stack_stats.kibana.plugins.telemetry.usage_fetcher).to.be.a('string'); + expect(stats.stack_stats.kibana.plugins.stack_management).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.ui_metric).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.application_usage).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.kql.defaultQueryLanguage).to.be.a('string'); + expect(stats.stack_stats.kibana.plugins['tsvb-validation']).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.localization).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.csp.strict).to.be(true); + expect(stats.stack_stats.kibana.plugins.csp.warnLegacyBrowsers).to.be(true); + expect(stats.stack_stats.kibana.plugins.csp.rulesChangedFromDefault).to.be(false); + }); + + it('should pull local stats and validate fields', async () => { + const timeRange = { + min: '2018-07-23T22:07:00Z', + max: '2018-07-23T22:13:00Z', + }; + + const { body } = await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ timeRange, unencrypted: true }) + .expect(200); + + const stats = body[0]; + + const actual = flatKeys(stats); + expect(actual).to.be.an('array'); + const expected = [ + 'cluster_name', + 'cluster_stats.cluster_uuid', + 'cluster_stats.indices.analysis', + 'cluster_stats.indices.completion', + 'cluster_stats.indices.count', + 'cluster_stats.indices.docs', + 'cluster_stats.indices.fielddata', + 'cluster_stats.indices.mappings', + 'cluster_stats.indices.query_cache', + 'cluster_stats.indices.segments', + 'cluster_stats.indices.shards', + 'cluster_stats.indices.store', + 'cluster_stats.nodes.count', + 'cluster_stats.nodes.discovery_types', + 'cluster_stats.nodes.fs', + 'cluster_stats.nodes.ingest', + 'cluster_stats.nodes.jvm', + 'cluster_stats.nodes.network_types', + 'cluster_stats.nodes.os', + 'cluster_stats.nodes.packaging_types', + 'cluster_stats.nodes.plugins', + 'cluster_stats.nodes.process', + 'cluster_stats.nodes.versions', + 'cluster_stats.status', + 'cluster_stats.timestamp', + 'cluster_uuid', + 'collection', + 'collectionSource', + 'stack_stats.kibana.count', + 'stack_stats.kibana.indices', + 'stack_stats.kibana.os', + 'stack_stats.kibana.plugins', + 'stack_stats.kibana.versions', + 'timestamp', + 'version', + ]; + + expect(expected.every(m => actual.includes(m))).to.be.ok(); + }); + }); +} diff --git a/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts new file mode 100644 index 0000000000000..4413c672fb46c --- /dev/null +++ b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { Client, DeleteDocumentParams, GetParams, GetResponse } from 'elasticsearch'; +import { TelemetrySavedObjectAttributes } from 'src/plugins/telemetry/server/telemetry_repository'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function optInTest({ getService }: FtrProviderContext) { + const client: Client = getService('legacyEs'); + const supertest = getService('supertest'); + + describe('/api/telemetry/v2/userHasSeenNotice API Telemetry User has seen OptIn Notice', () => { + it('should update telemetry setting field via PUT', async () => { + try { + await client.delete({ + index: '.kibana', + id: 'telemetry:telemetry', + } as DeleteDocumentParams); + } catch (err) { + if (err.statusCode !== 404) { + throw err; + } + } + + await supertest + .put('/api/telemetry/v2/userHasSeenNotice') + .set('kbn-xsrf', 'xxx') + .expect(200); + + const { + _source: { telemetry }, + }: GetResponse<{ + telemetry: TelemetrySavedObjectAttributes; + }> = await client.get({ + index: '.kibana', + id: 'telemetry:telemetry', + } as GetParams); + + expect(telemetry.userHasSeenNotice).to.be(true); + }); + }); +} diff --git a/test/functional/apps/discover/_discover_histogram.js b/test/functional/apps/discover/_discover_histogram.js index eeef3333aab0f..dcd185eba00e6 100644 --- a/test/functional/apps/discover/_discover_histogram.js +++ b/test/functional/apps/discover/_discover_histogram.js @@ -48,7 +48,7 @@ export default function({ getService, getPageObjects }) { log.debug('create long_window_logstash index pattern'); // NOTE: long_window_logstash load does NOT create index pattern - await PageObjects.settings.createIndexPattern('long-window-logstash-'); + await PageObjects.settings.createIndexPattern('long-window-logstash-*'); await kibanaServer.uiSettings.replace(defaultSettings); await browser.refresh(); diff --git a/test/functional/apps/getting_started/_shakespeare.js b/test/functional/apps/getting_started/_shakespeare.js index 9a4bb0081b7ad..3a3d6b93e166b 100644 --- a/test/functional/apps/getting_started/_shakespeare.js +++ b/test/functional/apps/getting_started/_shakespeare.js @@ -59,9 +59,9 @@ export default function({ getService, getPageObjects }) { it('should create shakespeare index pattern', async function() { log.debug('Create shakespeare index pattern'); - await PageObjects.settings.createIndexPattern('shakes', null); + await PageObjects.settings.createIndexPattern('shakespeare', null); const patternName = await PageObjects.settings.getIndexPageHeading(); - expect(patternName).to.be('shakes*'); + expect(patternName).to.be('shakespeare'); }); // https://www.elastic.co/guide/en/kibana/current/tutorial-visualizing.html @@ -74,7 +74,7 @@ export default function({ getService, getPageObjects }) { log.debug('create shakespeare vertical bar chart'); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVerticalBarChart(); - await PageObjects.visualize.clickNewSearch('shakes*'); + await PageObjects.visualize.clickNewSearch('shakespeare'); await PageObjects.visChart.waitForVisualization(); const expectedChartValues = [111396]; diff --git a/test/functional/apps/management/_index_pattern_create_delete.js b/test/functional/apps/management/_index_pattern_create_delete.js index 616e2297b2f51..2545b8f324d1b 100644 --- a/test/functional/apps/management/_index_pattern_create_delete.js +++ b/test/functional/apps/management/_index_pattern_create_delete.js @@ -44,10 +44,7 @@ export default function({ getService, getPageObjects }) { it('should handle special charaters in template input', async () => { await PageObjects.settings.clickAddNewIndexPatternButton(); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.settings.setIndexPatternField({ - indexPatternName: '❤️', - expectWildcard: false, - }); + await PageObjects.settings.setIndexPatternField('❤️'); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async () => { diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 862e5127bb670..93debdcc37f0a 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -44,6 +44,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl: boolean; shouldLoginIfPrompted: boolean; useActualUrl: boolean; + insertTimestamp: boolean; } class CommonPage { @@ -65,7 +66,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo * Logins to Kibana as default user and navigates to provided app * @param appUrl Kibana URL */ - private async loginIfPrompted(appUrl: string) { + private async loginIfPrompted(appUrl: string, insertTimestamp: boolean) { let currentUrl = await browser.getCurrentUrl(); log.debug(`currentUrl = ${currentUrl}\n appUrl = ${appUrl}`); await testSubjects.find('kibanaChrome', 6 * defaultFindTimeout); // 60 sec waiting @@ -87,7 +88,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo '[data-test-subj="kibanaChrome"] nav:not(.ng-hide)', 6 * defaultFindTimeout ); - await browser.get(appUrl); + await browser.get(appUrl, insertTimestamp); currentUrl = await browser.getCurrentUrl(); log.debug(`Finished login process currentUrl = ${currentUrl}`); } @@ -95,7 +96,13 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } private async navigate(navigateProps: NavigateProps) { - const { appConfig, ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl } = navigateProps; + const { + appConfig, + ensureCurrentUrl, + shouldLoginIfPrompted, + useActualUrl, + insertTimestamp, + } = navigateProps; const appUrl = getUrl.noAuth(config.get('servers.kibana'), appConfig); await retry.try(async () => { @@ -111,7 +118,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } const currentUrl = shouldLoginIfPrompted - ? await this.loginIfPrompted(appUrl) + ? await this.loginIfPrompted(appUrl, insertTimestamp) : await browser.getCurrentUrl(); if (ensureCurrentUrl && !currentUrl.includes(appUrl)) { @@ -134,6 +141,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl = true, shouldLoginIfPrompted = true, useActualUrl = false, + insertTimestamp = true, } = {} ) { const appConfig = { @@ -146,6 +154,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl, + insertTimestamp, }); } @@ -165,6 +174,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl = true, shouldLoginIfPrompted = true, useActualUrl = true, + insertTimestamp = true, } = {} ) { const appConfig = { @@ -178,6 +188,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl, + insertTimestamp, }); } @@ -208,7 +219,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo async navigateToApp( appName: string, - { basePath = '', shouldLoginIfPrompted = true, hash = '' } = {} + { basePath = '', shouldLoginIfPrompted = true, hash = '', insertTimestamp = true } = {} ) { let appUrl: string; if (config.has(['apps', appName])) { @@ -239,7 +250,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo log.debug('returned from get, calling refresh'); await browser.refresh(); let currentUrl = shouldLoginIfPrompted - ? await this.loginIfPrompted(appUrl) + ? await this.loginIfPrompted(appUrl, insertTimestamp) : await browser.getCurrentUrl(); if (currentUrl.includes('app/kibana')) { diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 8864eda3823ef..81d22838d1e8b 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -334,7 +334,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider } await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async () => { - await this.setIndexPatternField({ indexPatternName }); + await this.setIndexPatternField(indexPatternName); }); await PageObjects.common.sleep(2000); await (await this.getCreateIndexPatternGoToStep2Button()).click(); @@ -375,14 +375,32 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider return indexPatternId; } - async setIndexPatternField({ indexPatternName = 'logstash-', expectWildcard = true } = {}) { + async setIndexPatternField(indexPatternName = 'logstash-*') { log.debug(`setIndexPatternField(${indexPatternName})`); const field = await this.getIndexPatternField(); await field.clearValue(); - await field.type(indexPatternName, { charByChar: true }); + if ( + indexPatternName.charAt(0) === '*' && + indexPatternName.charAt(indexPatternName.length - 1) === '*' + ) { + // this is a special case when the index pattern name starts with '*' + // like '*:makelogs-*' where the UI will not append * + await field.type(indexPatternName, { charByChar: true }); + } else if (indexPatternName.charAt(indexPatternName.length - 1) === '*') { + // the common case where the UI will append '*' automatically so we won't type it + const tempName = indexPatternName.slice(0, -1); + await field.type(tempName, { charByChar: true }); + } else { + // case where we don't want the * appended so we'll remove it if it was added + await field.type(indexPatternName, { charByChar: true }); + const tempName = await field.getAttribute('value'); + if (tempName.length > indexPatternName.length) { + await field.type(browser.keys.DELETE, { charByChar: true }); + } + } const currentName = await field.getAttribute('value'); log.debug(`setIndexPatternField set to ${currentName}`); - expect(currentName).to.eql(`${indexPatternName}${expectWildcard ? '*' : ''}`); + expect(currentName).to.eql(indexPatternName); } async getCreateIndexPatternGoToStep2Button() { diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts new file mode 100644 index 0000000000000..5ea151dffdc8e --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { ExpectExpression, expectExpressionProvider } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +function getCell(esaggsResult: any, column: number, row: number): unknown | undefined { + const columnId = esaggsResult?.columns[column]?.id; + if (!columnId) { + return; + } + return esaggsResult?.rows[row]?.[columnId]; +} + +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; + describe('esaggs pipeline expression tests', () => { + before(() => { + expectExpression = expectExpressionProvider({ getService, updateBaselines }); + }); + + describe('correctly renders tagcloud', () => { + it('filters on index pattern primary date field by default', async () => { + const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }]; + const timeRange = { + from: '2006-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + const expression = ` + kibana_context timeRange='${JSON.stringify(timeRange)}' + | esaggs index='logstash-*' aggConfigs='${JSON.stringify(aggConfigs)}' + `; + const result = await expectExpression('esaggs_primary_timefield', expression).getResponse(); + expect(getCell(result, 0, 0)).to.be(9375); + }); + + it('filters on the specified date field', async () => { + const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }]; + const timeRange = { + from: '2006-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + const expression = ` + kibana_context timeRange='${JSON.stringify(timeRange)}' + | esaggs index='logstash-*' timeFields='relatedContent.article:published_time' aggConfigs='${JSON.stringify( + aggConfigs + )}' + `; + const result = await expectExpression('esaggs_other_timefield', expression).getResponse(); + expect(getCell(result, 0, 0)).to.be(11134); + }); + + it('filters on multiple specified date field', async () => { + const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }]; + const timeRange = { + from: '2006-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + const expression = ` + kibana_context timeRange='${JSON.stringify(timeRange)}' + | esaggs index='logstash-*' timeFields='relatedContent.article:published_time' timeFields='@timestamp' aggConfigs='${JSON.stringify( + aggConfigs + )}' + `; + const result = await expectExpression( + 'esaggs_multiple_timefields', + expression + ).getResponse(); + expect(getCell(result, 0, 0)).to.be(7452); + }); + }); + }); +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts index 031a0e3576ccc..9590f9f8c1794 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts @@ -46,5 +46,6 @@ export default function({ getService, getPageObjects, loadTestFile }: FtrProvide loadTestFile(require.resolve('./basic')); loadTestFile(require.resolve('./tag_cloud')); loadTestFile(require.resolve('./metric')); + loadTestFile(require.resolve('./esaggs')); }); } diff --git a/test/plugin_functional/test_suites/core_plugins/application_status.ts b/test/plugin_functional/test_suites/core_plugins/application_status.ts index b6d13a5604011..c384e41851e15 100644 --- a/test/plugin_functional/test_suites/core_plugins/application_status.ts +++ b/test/plugin_functional/test_suites/core_plugins/application_status.ts @@ -17,6 +17,7 @@ * under the License. */ +import url from 'url'; import expect from '@kbn/expect'; import { AppNavLinkStatus, @@ -26,6 +27,15 @@ import { import { PluginFunctionalProviderContext } from '../../services'; import '../../plugins/core_app_status/public/types'; +const getKibanaUrl = (pathname?: string, search?: string) => + url.format({ + protocol: 'http:', + hostname: process.env.TEST_KIBANA_HOST || 'localhost', + port: process.env.TEST_KIBANA_PORT || '5620', + pathname, + search, + }); + // eslint-disable-next-line import/no-default-export export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { const PageObjects = getPageObjects(['common']); @@ -97,6 +107,22 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider expect(await testSubjects.exists('appStatusApp')).to.eql(true); }); + it('allows to change the defaultPath of an application', async () => { + let link = await appsMenu.getLink('App Status'); + expect(link!.href).to.eql(getKibanaUrl('/app/app_status')); + + await setAppStatus({ + defaultPath: '/arbitrary/path', + }); + + link = await appsMenu.getLink('App Status'); + expect(link!.href).to.eql(getKibanaUrl('/app/app_status/arbitrary/path')); + + await navigateToApp('app_status'); + expect(await testSubjects.exists('appStatusApp')).to.eql(true); + expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/app_status/arbitrary/path')); + }); + it('can change the state of the currently mounted app', async () => { await setAppStatus({ status: AppStatus.accessible, diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 67d88b308ed91..951ba8e22d885 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -39,7 +39,7 @@ else # build runtime for canvas echo "NODE_ENV=$NODE_ENV" node ./legacy/plugins/canvas/scripts/shareable_runtime - node --max-old-space-size=6144 scripts/jest --ci --verbose --coverage + node --max-old-space-size=6144 scripts/jest --ci --verbose --detectOpenHandles --coverage # rename file in order to be unique one test -f ../target/kibana-coverage/jest/coverage-final.json \ && mv ../target/kibana-coverage/jest/coverage-final.json \ diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index ae1a55254c702..4acb170d12574 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -43,7 +43,7 @@ "xpack.transform": "plugins/transform", "xpack.triggersActionsUI": "plugins/triggers_actions_ui", "xpack.upgradeAssistant": "plugins/upgrade_assistant", - "xpack.uptime": ["plugins/uptime", "legacy/plugins/uptime"], + "xpack.uptime": ["plugins/uptime"], "xpack.watcher": "plugins/watcher" }, "translations": [ diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index af5ace8e3cd3b..4f1251321b005 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -34,6 +34,20 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { '^test_utils/stub_web_worker': `${xPackKibanaDirectory}/test_utils/stub_web_worker.ts`, '^(!!)?file-loader!': fileMockPath, }, + collectCoverageFrom: [ + 'legacy/plugins/**/*.{js,jsx,ts,tsx}', + 'legacy/server/**/*.{js,jsx,ts,tsx}', + 'plugins/**/*.{js,jsx,ts,tsx}', + '!**/{__test__,__snapshots__,__examples__,integration_tests,tests}/**', + '!**/*.test.{js,ts,tsx}', + '!**/flot-charts/**', + '!**/test/**', + '!**/build/**', + '!**/scripts/**', + '!**/mocks/**', + '!**/plugins/apm/e2e/**', + ], + coveragePathIgnorePatterns: ['.*\\.d\\.ts'], coverageDirectory: '/../target/kibana-coverage/jest', coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html'], setupFiles: [ diff --git a/x-pack/index.js b/x-pack/index.js index fe4c325405933..cfadddac3994a 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -16,12 +16,8 @@ import { spaces } from './legacy/plugins/spaces'; import { canvas } from './legacy/plugins/canvas'; import { infra } from './legacy/plugins/infra'; import { taskManager } from './legacy/plugins/task_manager'; -import { uptime } from './legacy/plugins/uptime'; import { encryptedSavedObjects } from './legacy/plugins/encrypted_saved_objects'; -import { actions } from './legacy/plugins/actions'; -import { alerting } from './legacy/plugins/alerting'; import { ingestManager } from './legacy/plugins/ingest_manager'; -import { triggersActionsUI } from './legacy/plugins/triggers_actions_ui'; module.exports = function(kibana) { return [ @@ -37,11 +33,7 @@ module.exports = function(kibana) { canvas(kibana), infra(kibana), taskManager(kibana), - uptime(kibana), encryptedSavedObjects(kibana), - actions(kibana), - alerting(kibana), ingestManager(kibana), - triggersActionsUI(kibana), ]; }; diff --git a/x-pack/legacy/plugins/actions/server/index.ts b/x-pack/legacy/plugins/actions/server/index.ts deleted file mode 100644 index 63dd6f99f9c24..0000000000000 --- a/x-pack/legacy/plugins/actions/server/index.ts +++ /dev/null @@ -1,38 +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 { Root } from 'joi'; -import { Legacy } from 'kibana'; -import mappings from './mappings.json'; -import { - LegacyPluginApi, - LegacyPluginSpec, - ArrayOrItem, -} from '../../../../../src/legacy/plugin_discovery/types'; - -export function actions(kibana: LegacyPluginApi): ArrayOrItem { - return new kibana.Plugin({ - id: 'actions', - configPrefix: 'xpack.actions', - config(Joi: Root) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }) - .unknown(true) - .default(); - }, - require: ['kibana', 'elasticsearch'], - isEnabled(config: Legacy.KibanaConfig) { - return ( - config.get('xpack.encryptedSavedObjects.enabled') === true && - config.get('xpack.actions.enabled') === true && - config.get('xpack.task_manager.enabled') === true - ); - }, - uiExports: { - mappings, - }, - } as Legacy.PluginSpecOptions); -} diff --git a/x-pack/legacy/plugins/alerting/server/index.ts b/x-pack/legacy/plugins/alerting/server/index.ts deleted file mode 100644 index 065af7dedebd9..0000000000000 --- a/x-pack/legacy/plugins/alerting/server/index.ts +++ /dev/null @@ -1,40 +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 { Legacy } from 'kibana'; -import { Root } from 'joi'; -import mappings from './mappings.json'; -import { - LegacyPluginApi, - LegacyPluginSpec, - ArrayOrItem, -} from '../../../../../src/legacy/plugin_discovery/types'; - -export function alerting(kibana: LegacyPluginApi): ArrayOrItem { - return new kibana.Plugin({ - id: 'alerting', - configPrefix: 'xpack.alerting', - require: ['kibana', 'elasticsearch', 'actions', 'task_manager', 'encryptedSavedObjects'], - isEnabled(config: Legacy.KibanaConfig) { - return ( - config.get('xpack.alerting.enabled') === true && - config.get('xpack.actions.enabled') === true && - config.get('xpack.encryptedSavedObjects.enabled') === true && - config.get('xpack.task_manager.enabled') === true - ); - }, - config(Joi: Root) { - return Joi.object() - .keys({ - enabled: Joi.boolean().default(true), - }) - .default(); - }, - uiExports: { - mappings, - }, - } as Legacy.PluginSpecOptions); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx index 27095264461ee..f57ddb5cf69a2 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx @@ -71,10 +71,10 @@ function getSpanTypes(span: Span) { }; } -const SpanBadge = styled(EuiBadge)` +const SpanBadge = (styled(EuiBadge)` display: inline-block; margin-right: ${px(units.quarter)}; -` as any; +` as unknown) as typeof EuiBadge; const HttpInfoContainer = styled('div')` margin-right: ${px(units.quarter)}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx index a5d8902ff1626..f01b2aa335a3a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx @@ -10,10 +10,10 @@ import React from 'react'; import styled from 'styled-components'; import { px, units } from '../../../../../../style/variables'; -const SpanBadge = styled(EuiBadge)` +const SpanBadge = (styled(EuiBadge)` display: inline-block; margin-right: ${px(units.quarter)}; -` as any; +` as unknown) as typeof EuiBadge; interface SyncBadgeProps { /** diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx index 858e6d29bfa5e..2be3c82a8385b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx @@ -17,7 +17,7 @@ interface Props { const Badge = (styled(EuiBadge)` margin-top: ${px(units.eighth)}; -` as any) as any; +` as unknown) as typeof EuiBadge; export const ErrorCountSummaryItemBadge = ({ count }: Props) => ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx index 1e1d49b2cf417..d499dddeeb8b3 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx @@ -13,7 +13,7 @@ import { HttpStatusBadge } from '../HttpStatusBadge'; const HttpInfoBadge = (styled(EuiBadge)` margin-right: ${px(units.quarter)}; -` as any) as any; +` as unknown) as typeof EuiBadge; const Url = styled('span')` display: inline-block; diff --git a/x-pack/legacy/plugins/canvas/public/components/app/track_route_change.js b/x-pack/legacy/plugins/canvas/public/components/app/track_route_change.js index e837f5200a159..2886aa868eb9e 100644 --- a/x-pack/legacy/plugins/canvas/public/components/app/track_route_change.js +++ b/x-pack/legacy/plugins/canvas/public/components/app/track_route_change.js @@ -7,13 +7,17 @@ import { get } from 'lodash'; import { getWindow } from '../../lib/get_window'; import { CANVAS_APP } from '../../../common/lib/constants'; -import { getCoreStart, getStartPlugins } from '../../legacy'; +import { platformService } from '../../services'; export function trackRouteChange() { - const basePath = getCoreStart().http.basePath.get(); - // storage.set(LOCALSTORAGE_LASTPAGE, pathname); - getStartPlugins().__LEGACY.trackSubUrlForApp( - CANVAS_APP, - getStartPlugins().__LEGACY.absoluteToParsedUrl(get(getWindow(), 'location.href'), basePath) - ); + const basePath = platformService.getService().coreStart.http.basePath.get(); + + platformService + .getService() + .startPlugins.__LEGACY.trackSubUrlForApp( + CANVAS_APP, + platformService + .getService() + .startPlugins.__LEGACY.absoluteToParsedUrl(get(getWindow(), 'location.href'), basePath) + ); } diff --git a/x-pack/legacy/plugins/canvas/public/legacy.ts b/x-pack/legacy/plugins/canvas/public/legacy.ts index 5bb628909c32e..f83887bbcbdfd 100644 --- a/x-pack/legacy/plugins/canvas/public/legacy.ts +++ b/x-pack/legacy/plugins/canvas/public/legacy.ts @@ -35,8 +35,6 @@ const shimStartPlugins: CanvasStartDeps = { __LEGACY: { // ToDo: Copy directly into canvas absoluteToParsedUrl, - // ToDo: Copy directly into canvas - formatMsg, // 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/breadcrumbs.ts b/x-pack/legacy/plugins/canvas/public/lib/breadcrumbs.ts index 834d5868c35ea..57b513affd781 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/breadcrumbs.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/breadcrumbs.ts @@ -5,7 +5,7 @@ */ import { ChromeBreadcrumb } from '../../../../../../src/core/public'; -import { getCoreStart } from '../legacy'; +import { platformService } from '../services'; export const getBaseBreadcrumb = () => ({ text: 'Canvas', @@ -24,6 +24,6 @@ export const getWorkpadBreadcrumb = ({ }; export const setBreadcrumb = (paths: ChromeBreadcrumb | ChromeBreadcrumb[]) => { - const setBreadCrumbs = getCoreStart().chrome.setBreadcrumbs; + const setBreadCrumbs = platformService.getService().coreStart.chrome.setBreadcrumbs; setBreadCrumbs(Array.isArray(paths) ? paths : [paths]); }; diff --git a/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts b/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts index 4118bb81b8363..8952802dc2f2b 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts @@ -8,10 +8,10 @@ import { AxiosPromise } from 'axios'; import { API_ROUTE_CUSTOM_ELEMENT } from '../../common/lib/constants'; import { fetch } from '../../common/lib/fetch'; import { CustomElement } from '../../types'; -import { getCoreStart } from '../legacy'; +import { platformService } from '../services'; const getApiPath = function() { - const basePath = getCoreStart().http.basePath.get(); + const basePath = platformService.getService().coreStart.http.basePath.get(); return `${basePath}${API_ROUTE_CUSTOM_ELEMENT}`; }; diff --git a/x-pack/legacy/plugins/canvas/public/lib/documentation_links.ts b/x-pack/legacy/plugins/canvas/public/lib/documentation_links.ts index 40e09ee39d714..6430f7d87d4f7 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/documentation_links.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/documentation_links.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getCoreStart } from '../legacy'; +import { platformService } from '../services'; export const getDocumentationLinks = () => ({ - canvas: `${getCoreStart().docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${ - getCoreStart().docLinks.DOC_LINK_VERSION + canvas: `${platformService.getService().coreStart.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${ + platformService.getService().coreStart.docLinks.DOC_LINK_VERSION }/canvas.html`, - numeral: `${getCoreStart().docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${ - getCoreStart().docLinks.DOC_LINK_VERSION + numeral: `${platformService.getService().coreStart.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${ + platformService.getService().coreStart.docLinks.DOC_LINK_VERSION }/guide/numeral.html`, }); diff --git a/x-pack/legacy/plugins/canvas/public/lib/es_service.ts b/x-pack/legacy/plugins/canvas/public/lib/es_service.ts index 6aa4968f29155..184f4f3c8af7c 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/es_service.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/es_service.ts @@ -11,21 +11,21 @@ import { API_ROUTE } from '../../common/lib/constants'; import { fetch } from '../../common/lib/fetch'; import { ErrorStrings } from '../../i18n'; import { notifyService } from '../services'; -import { getCoreStart } from '../legacy'; +import { platformService } from '../services'; const { esService: strings } = ErrorStrings; const getApiPath = function() { - const basePath = getCoreStart().http.basePath.get(); + const basePath = platformService.getService().coreStart.http.basePath.get(); return basePath + API_ROUTE; }; const getSavedObjectsClient = function() { - return getCoreStart().savedObjects.client; + return platformService.getService().coreStart.savedObjects.client; }; const getAdvancedSettings = function() { - return getCoreStart().uiSettings; + return platformService.getService().coreStart.uiSettings; }; export const getFields = (index = '_all') => { diff --git a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js index f3681f50c56a5..e6628399f53c2 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js +++ b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js @@ -11,7 +11,7 @@ import { DEFAULT_WORKPAD_CSS, } from '../../common/lib/constants'; import { fetch } from '../../common/lib/fetch'; -import { getCoreStart } from '../legacy'; +import { platformService } from '../services'; /* Remove any top level keys from the workpad which will be rejected by validation */ @@ -43,17 +43,17 @@ const sanitizeWorkpad = function(workpad) { }; const getApiPath = function() { - const basePath = getCoreStart().http.basePath.get(); + const basePath = platformService.getService().coreStart.http.basePath.get(); return `${basePath}${API_ROUTE_WORKPAD}`; }; const getApiPathStructures = function() { - const basePath = getCoreStart().http.basePath.get(); + const basePath = platformService.getService().coreStart.http.basePath.get(); return `${basePath}${API_ROUTE_WORKPAD_STRUCTURES}`; }; const getApiPathAssets = function() { - const basePath = getCoreStart().http.basePath.get(); + const basePath = platformService.getService().coreStart.http.basePath.get(); return `${basePath}${API_ROUTE_WORKPAD_ASSETS}`; }; diff --git a/x-pack/legacy/plugins/canvas/public/plugin.tsx b/x-pack/legacy/plugins/canvas/public/plugin.tsx index 36ce1974be272..baeb4ebd453d2 100644 --- a/x-pack/legacy/plugins/canvas/public/plugin.tsx +++ b/x-pack/legacy/plugins/canvas/public/plugin.tsx @@ -44,7 +44,6 @@ export interface CanvasStartDeps { uiActions: UiActionsStart; __LEGACY: { absoluteToParsedUrl: (url: string, basePath: string) => any; - formatMsg: any; trackSubUrlForApp: Chrome['trackSubUrlForApp']; }; } @@ -64,6 +63,7 @@ export class CanvasPlugin implements Plugin { // TODO: Do we want to completely move canvas_plugin_src into it's own plugin? private srcPlugin = new CanvasSrcPlugin(); + private startPlugins: CanvasStartDeps | undefined; public setup(core: CoreSetup, plugins: CanvasSetupDeps) { const { api: canvasApi, registries } = getPluginApi(plugins.expressions); @@ -73,14 +73,26 @@ export class CanvasPlugin core.application.register({ id: 'canvas', title: 'Canvas App', - async mount(context, params) { + mount: async (context, params) => { // Load application bundle const { renderApp, initializeCanvas, teardownCanvas } = await import('./application'); // Get start services const [coreStart, depsStart] = await core.getStartServices(); - const canvasStore = await initializeCanvas(core, coreStart, plugins, depsStart, registries); + // TODO: We only need this to get the __LEGACY stuff that isn't coming from getStartSevices. + // We won't need this as soon as we move over to NP Completely + if (!this.startPlugins) { + throw new Error('Start Plugins not ready at mount time'); + } + + const canvasStore = await initializeCanvas( + core, + coreStart, + plugins, + this.startPlugins, + registries + ); const unmount = renderApp(coreStart, depsStart, params, canvasStore); @@ -115,6 +127,7 @@ export class CanvasPlugin } public start(core: CoreStart, plugins: CanvasStartDeps) { + this.startPlugins = plugins; this.srcPlugin.start(core, plugins); initLoadingIndicator(core.http.addLoadingCountSource); } diff --git a/x-pack/legacy/plugins/canvas/public/services/index.ts b/x-pack/legacy/plugins/canvas/public/services/index.ts index 12c0a687bf308..17d836f1441c9 100644 --- a/x-pack/legacy/plugins/canvas/public/services/index.ts +++ b/x-pack/legacy/plugins/canvas/public/services/index.ts @@ -7,6 +7,7 @@ import { CoreSetup, CoreStart } from '../../../../../../src/core/public'; import { CanvasSetupDeps, CanvasStartDeps } from '../plugin'; import { notifyServiceFactory } from './notify'; +import { platformServiceFactory } from './platform'; export type CanvasServiceFactory = ( coreSetup: CoreSetup, @@ -49,6 +50,7 @@ export type ServiceFromProvider

= P extends CanvasServiceProvider ? export const services = { notify: new CanvasServiceProvider(notifyServiceFactory), + platform: new CanvasServiceProvider(platformServiceFactory), }; export interface CanvasServices { @@ -70,4 +72,4 @@ export const stopServices = () => { Object.entries(services).forEach(([key, provider]) => provider.stop()); }; -export const { notify: notifyService } = services; +export const { notify: notifyService, platform: platformService } = services; diff --git a/x-pack/legacy/plugins/canvas/public/services/platform.ts b/x-pack/legacy/plugins/canvas/public/services/platform.ts new file mode 100644 index 0000000000000..440e9523044c1 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/services/platform.ts @@ -0,0 +1,24 @@ +/* + * 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 { CanvasServiceFactory } from '.'; +import { CoreStart, CoreSetup, CanvasSetupDeps, CanvasStartDeps } from '../plugin'; + +interface PlatformService { + coreSetup: CoreSetup; + coreStart: CoreStart; + setupPlugins: CanvasSetupDeps; + startPlugins: CanvasStartDeps; +} + +export const platformServiceFactory: CanvasServiceFactory = ( + coreSetup, + coreStart, + setupPlugins, + startPlugins +) => { + return { coreSetup, coreStart, setupPlugins, startPlugins }; +}; diff --git a/x-pack/legacy/plugins/canvas/public/state/initial_state.js b/x-pack/legacy/plugins/canvas/public/state/initial_state.js index 40c017543147f..bfa68b33908e0 100644 --- a/x-pack/legacy/plugins/canvas/public/state/initial_state.js +++ b/x-pack/legacy/plugins/canvas/public/state/initial_state.js @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { getCoreStart } from '../legacy'; +import { platformService } from '../services'; import { getDefaultWorkpad } from './defaults'; export const getInitialState = path => { @@ -13,7 +13,7 @@ export const getInitialState = path => { app: {}, // Kibana stuff in here assets: {}, // assets end up here transient: { - canUserWrite: getCoreStart().application.capabilities.canvas.save, + canUserWrite: platformService.getService().coreStart.application.capabilities.canvas.save, zoomScale: 1, elementStats: { total: 0, diff --git a/x-pack/legacy/plugins/canvas/public/state/reducers/workpad.js b/x-pack/legacy/plugins/canvas/public/state/reducers/workpad.js index 12733680ed32d..30f9c638a054f 100644 --- a/x-pack/legacy/plugins/canvas/public/state/reducers/workpad.js +++ b/x-pack/legacy/plugins/canvas/public/state/reducers/workpad.js @@ -5,7 +5,7 @@ */ import { handleActions } from 'redux-actions'; -import { getCoreStart } from '../../legacy'; +import { platformService } from '../../services'; import { getDefaultWorkpad } from '../defaults'; import { setWorkpad, @@ -22,11 +22,13 @@ import { APP_ROUTE_WORKPAD } from '../../../common/lib/constants'; export const workpadReducer = handleActions( { [setWorkpad]: (workpadState, { payload }) => { - getCoreStart().chrome.recentlyAccessed.add( - `${APP_ROUTE_WORKPAD}/${payload.id}`, - payload.name, - payload.id - ); + platformService + .getService() + .coreStart.chrome.recentlyAccessed.add( + `${APP_ROUTE_WORKPAD}/${payload.id}`, + payload.name, + payload.id + ); return payload; }, @@ -39,11 +41,13 @@ export const workpadReducer = handleActions( }, [setName]: (workpadState, { payload }) => { - getCoreStart().chrome.recentlyAccessed.add( - `${APP_ROUTE_WORKPAD}/${workpadState.id}`, - payload, - workpadState.id - ); + platformService + .getService() + .coreStart.chrome.recentlyAccessed.add( + `${APP_ROUTE_WORKPAD}/${workpadState.id}`, + payload, + workpadState.id + ); return { ...workpadState, name: payload }; }, diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index d1e8892fa2c98..a1186e04ee27a 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -54,7 +54,7 @@ export function maps(kibana) { emsLandingPageUrl: mapConfig.emsLandingPageUrl, kbnPkgVersion: serverConfig.get('pkg.version'), regionmapLayers: _.get(mapConfig, 'regionmap.layers', []), - tilemap: _.get(mapConfig, 'tilemap', []), + tilemap: _.get(mapConfig, 'tilemap', {}), }; }, styleSheetPaths: `${__dirname}/public/index.scss`, diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 1b1fbf111fe04..bb1a6b74d43a7 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -336,7 +336,7 @@ app.controller( function addFilters(newFilters) { newFilters.forEach(filter => { - filter.$state = esFilters.FilterStateStore.APP_STATE; + filter.$state = { store: esFilters.FilterStateStore.APP_STATE }; }); $scope.updateFiltersAndDispatch([...$scope.filters, ...newFilters]); } diff --git a/x-pack/legacy/plugins/monitoring/common/constants.ts b/x-pack/legacy/plugins/monitoring/common/constants.ts index 3a4c7b71dcd03..36030e1fa7f2a 100644 --- a/x-pack/legacy/plugins/monitoring/common/constants.ts +++ b/x-pack/legacy/plugins/monitoring/common/constants.ts @@ -251,7 +251,7 @@ export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION, ALERT_TYPE_CLUSTER_ST /** * Matches the id for the built-in in email action type - * See x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts + * See x-pack/plugins/actions/server/builtin_action_types/email.ts */ export const ALERT_ACTION_TYPE_EMAIL = '.email'; diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts index a047c25c2b1d7..c6031cb220334 100644 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts +++ b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts @@ -5,6 +5,8 @@ */ import angular, { IWindowService } from 'angular'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext } from 'kibana/public'; diff --git a/x-pack/legacy/plugins/security/index.ts b/x-pack/legacy/plugins/security/index.ts index 5b2218af1fd52..b1dec2ce82c52 100644 --- a/x-pack/legacy/plugins/security/index.ts +++ b/x-pack/legacy/plugins/security/index.ts @@ -78,9 +78,7 @@ export const security = (kibana: Record) => // features are up to date. xpackInfo .feature(this.id) - .registerLicenseCheckResultsGenerator(() => - securityPlugin.__legacyCompat.license.getFeatures() - ); + .registerLicenseCheckResultsGenerator(() => securityPlugin.license.getFeatures()); server.expose({ getUser: async (request: LegacyRequest) => diff --git a/x-pack/legacy/plugins/task_manager/server/index.ts b/x-pack/legacy/plugins/task_manager/server/index.ts index 3ea687f7003f4..a3167920efa06 100644 --- a/x-pack/legacy/plugins/task_manager/server/index.ts +++ b/x-pack/legacy/plugins/task_manager/server/index.ts @@ -6,8 +6,6 @@ import { Root } from 'joi'; import { Legacy } from 'kibana'; -import mappings from './mappings.json'; -import { migrations } from './migrations'; import { createLegacyApi, getTaskManagerSetup } from './legacy'; export { LegacyTaskManagerApi, getTaskManagerSetup, getTaskManagerStart } from './legacy'; @@ -21,19 +19,6 @@ import { ArrayOrItem, } from '../../../../../src/legacy/plugin_discovery/types'; -const savedObjectSchemas = { - task: { - hidden: true, - isNamespaceAgnostic: true, - convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, - // legacy config is marked as any in core, no choice here - // eslint-disable-next-line @typescript-eslint/no-explicit-any - indexPattern(config: any) { - return config.get('xpack.task_manager.index'); - }, - }, -}; - export function taskManager(kibana: LegacyPluginApi): ArrayOrItem { return new kibana.Plugin({ id: 'task_manager', @@ -58,7 +43,7 @@ export function taskManager(kibana: LegacyPluginApi): ArrayOrItem { // we can't tell the Kibana Platform Task Manager plugin to // to wait to `start` as that happens before legacy plugins @@ -77,10 +62,5 @@ export function taskManager(kibana: LegacyPluginApi): ArrayOrItem - new kibana.Plugin({ - configPrefix: 'xpack.uptime', - id: PLUGIN.ID, - publicDir: resolve(__dirname, 'public'), - require: ['alerting', 'kibana', 'elasticsearch', 'xpack_main'], - uiExports: { - app: { - description: i18n.translate('xpack.uptime.pluginDescription', { - defaultMessage: 'Uptime monitoring', - description: 'The description text that will be shown to users in Kibana', - }), - icon: 'plugins/uptime/icons/heartbeat_white.svg', - euiIconType: 'uptimeApp', - title: i18n.translate('xpack.uptime.uptimeFeatureCatalogueTitle', { - defaultMessage: 'Uptime', - }), - main: 'plugins/uptime/app', - order: 8900, - url: '/app/uptime#/', - category: DEFAULT_APP_CATEGORIES.observability, - }, - home: ['plugins/uptime/register_feature'], - }, - }); diff --git a/x-pack/legacy/plugins/uptime/public/apps/index.ts b/x-pack/legacy/plugins/uptime/public/apps/index.ts deleted file mode 100644 index d58bf8398fcde..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/apps/index.ts +++ /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 { npSetup } from 'ui/new_platform'; -import { Plugin } from './plugin'; -import 'uiExports/embeddableFactories'; - -const plugin = new Plugin({ - opaqueId: Symbol('uptime'), - env: {} as any, - config: { get: () => ({} as any) }, -}); -plugin.setup(npSetup); diff --git a/x-pack/legacy/plugins/uptime/public/apps/plugin.ts b/x-pack/legacy/plugins/uptime/public/apps/plugin.ts deleted file mode 100644 index e73598c44c9f0..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/apps/plugin.ts +++ /dev/null @@ -1,56 +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 { LegacyCoreSetup, PluginInitializerContext, AppMountParameters } from 'src/core/public'; -import { PluginsSetup } from 'ui/new_platform/new_platform'; -import { FeatureCatalogueCategory } from '../../../../../../src/plugins/home/public'; -import { UMFrontendLibs } from '../lib/lib'; -import { PLUGIN } from '../../common/constants'; -import { getKibanaFrameworkAdapter } from '../lib/adapters/framework/new_platform_adapter'; - -export interface SetupObject { - core: LegacyCoreSetup; - plugins: PluginsSetup; -} - -export class Plugin { - constructor( - // @ts-ignore this is added to satisfy the New Platform typing constraint, - // but we're not leveraging any of its functionality yet. - private readonly initializerContext: PluginInitializerContext - ) {} - - public setup(setup: SetupObject) { - const { core, plugins } = setup; - const { home } = plugins; - - home.featureCatalogue.register({ - category: FeatureCatalogueCategory.DATA, - description: PLUGIN.DESCRIPTION, - icon: 'uptimeApp', - id: PLUGIN.ID, - path: '/app/uptime#/', - showOnHomePage: true, - title: PLUGIN.TITLE, - }); - - core.application.register({ - id: PLUGIN.ID, - euiIconType: 'uptimeApp', - order: 8900, - title: 'Uptime', - async mount(params: AppMountParameters) { - const [coreStart] = await core.getStartServices(); - const { element } = params; - const libs: UMFrontendLibs = { - framework: getKibanaFrameworkAdapter(coreStart, plugins), - }; - libs.framework.render(element); - return () => {}; - }, - }); - } -} diff --git a/x-pack/legacy/plugins/uptime/public/register_feature.ts b/x-pack/legacy/plugins/uptime/public/register_feature.ts deleted file mode 100644 index 2f83fa33ba4bc..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/register_feature.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 { i18n } from '@kbn/i18n'; -import { npSetup } from 'ui/new_platform'; -import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; - -const { - plugins: { home }, -} = npSetup; - -home.featureCatalogue.register({ - id: 'uptime', - title: i18n.translate('xpack.uptime.uptimeFeatureCatalogueTitle', { defaultMessage: 'Uptime' }), - description: i18n.translate('xpack.uptime.featureCatalogueDescription', { - defaultMessage: 'Perform endpoint health checks and uptime monitoring.', - }), - icon: 'uptimeApp', - path: `uptime#/`, - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA, -}); diff --git a/x-pack/legacy/plugins/uptime/tsconfig.json b/x-pack/legacy/plugins/uptime/tsconfig.json deleted file mode 100644 index 53425909db3e8..0000000000000 --- a/x-pack/legacy/plugins/uptime/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "exclude": ["**/node_modules/**"], - "paths": { - "react": ["../../../node_modules/@types/react"] - } -} \ No newline at end of file diff --git a/x-pack/package.json b/x-pack/package.json index 604889c6094b9..dcc9b8c61cb96 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -105,6 +105,7 @@ "@types/recompose": "^0.30.6", "@types/reduce-reducers": "^1.0.0", "@types/redux-actions": "^2.6.1", + "@types/set-value": "^2.0.0", "@types/sinon": "^7.0.13", "@types/styled-components": "^4.4.2", "@types/supertest": "^2.0.5", @@ -344,6 +345,7 @@ "rison-node": "0.3.1", "rxjs": "^6.5.3", "semver": "5.7.0", + "set-value": "^3.0.2", "squel": "^5.13.0", "stats-lite": "^2.2.0", "style-it": "^2.1.3", diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 101e18f2583e3..3e9262c05efac 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -17,7 +17,7 @@ import { import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { SpacesServiceSetup } from '../../../spaces/server'; import { EVENT_LOG_ACTIONS } from '../plugin'; -import { IEvent, IEventLogger } from '../../../event_log/server'; +import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; export interface ActionExecutorContext { logger: Logger; @@ -110,7 +110,16 @@ export class ActionExecutor { const actionLabel = `${actionTypeId}:${actionId}: ${name}`; const event: IEvent = { event: { action: EVENT_LOG_ACTIONS.execute }, - kibana: { saved_objects: [{ type: 'action', id: actionId, ...namespace }] }, + kibana: { + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: 'action', + id: actionId, + ...namespace, + }, + ], + }, }; eventLogger.startTiming(event); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 10e4d64584340..a6cc1fb5463bb 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -53,6 +53,7 @@ import { } from './routes'; import { IEventLogger, IEventLogService } from '../../event_log/server'; import { initializeActionsTelemetry, scheduleActionsTelemetry } from './usage/task'; +import { setupSavedObjects } from './saved_objects'; const EVENT_LOG_PROVIDER = 'actions'; export const EVENT_LOG_ACTIONS = { @@ -133,19 +134,7 @@ export class ActionsPlugin implements Plugin, Plugi ); } - // Encrypted attributes - // - `secrets` properties will be encrypted - // - `config` will be included in AAD - // - everything else excluded from AAD - plugins.encryptedSavedObjects.registerType({ - type: 'action', - attributesToEncrypt: new Set(['secrets']), - attributesToExcludeFromAAD: new Set(['name']), - }); - plugins.encryptedSavedObjects.registerType({ - type: 'action_task_params', - attributesToEncrypt: new Set(['apiKey']), - }); + setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects); plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS)); this.eventLogger = plugins.eventLog.getLogger({ diff --git a/x-pack/plugins/actions/server/saved_objects/index.ts b/x-pack/plugins/actions/server/saved_objects/index.ts new file mode 100644 index 0000000000000..dbd7925f96871 --- /dev/null +++ b/x-pack/plugins/actions/server/saved_objects/index.ts @@ -0,0 +1,42 @@ +/* + * 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 { SavedObjectsServiceSetup } from 'kibana/server'; +import mappings from './mappings.json'; +import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; + +export function setupSavedObjects( + savedObjects: SavedObjectsServiceSetup, + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +) { + savedObjects.registerType({ + name: 'action', + hidden: false, + namespaceType: 'single', + mappings: mappings.action, + }); + + // Encrypted attributes + // - `secrets` properties will be encrypted + // - `config` will be included in AAD + // - everything else excluded from AAD + encryptedSavedObjects.registerType({ + type: 'action', + attributesToEncrypt: new Set(['secrets']), + attributesToExcludeFromAAD: new Set(['name']), + }); + + savedObjects.registerType({ + name: 'action_task_params', + hidden: false, + namespaceType: 'single', + mappings: mappings.action_task_params, + }); + encryptedSavedObjects.registerType({ + type: 'action_task_params', + attributesToEncrypt: new Set(['apiKey']), + }); +} diff --git a/x-pack/legacy/plugins/actions/server/mappings.json b/x-pack/plugins/actions/server/saved_objects/mappings.json similarity index 100% rename from x-pack/legacy/plugins/actions/server/mappings.json rename to x-pack/plugins/actions/server/saved_objects/mappings.json diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index c03d3506a051d..8cdde2eeb9877 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -58,6 +58,7 @@ import { Services } from './types'; import { registerAlertsUsageCollector } from './usage'; import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/task'; import { IEventLogger, IEventLogService } from '../../event_log/server'; +import { setupSavedObjects } from './saved_objects'; const EVENT_LOG_PROVIDER = 'alerting'; export const EVENT_LOG_ACTIONS = { @@ -134,17 +135,7 @@ export class AlertingPlugin { ); } - // Encrypted attributes - plugins.encryptedSavedObjects.registerType({ - type: 'alert', - attributesToEncrypt: new Set(['apiKey']), - attributesToExcludeFromAAD: new Set([ - 'scheduledTaskId', - 'muteAll', - 'mutedInstanceIds', - 'updatedBy', - ]), - }); + setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects); plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS)); this.eventLogger = plugins.eventLog.getLogger({ diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts new file mode 100644 index 0000000000000..4efec2fe55ef0 --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -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 { SavedObjectsServiceSetup } from 'kibana/server'; +import mappings from './mappings.json'; +import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; + +export function setupSavedObjects( + savedObjects: SavedObjectsServiceSetup, + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +) { + savedObjects.registerType({ + name: 'alert', + hidden: false, + namespaceType: 'single', + mappings: mappings.alert, + }); + + // Encrypted attributes + encryptedSavedObjects.registerType({ + type: 'alert', + attributesToEncrypt: new Set(['apiKey']), + attributesToExcludeFromAAD: new Set([ + 'scheduledTaskId', + 'muteAll', + 'mutedInstanceIds', + 'updatedBy', + ]), + }); +} diff --git a/x-pack/legacy/plugins/alerting/server/mappings.json b/x-pack/plugins/alerting/server/saved_objects/mappings.json similarity index 100% rename from x-pack/legacy/plugins/alerting/server/mappings.json rename to x-pack/plugins/alerting/server/saved_objects/mappings.json diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 0e46ef4919626..a564b87f2ca50 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -95,6 +95,7 @@ test('calls actionsPlugin.execute per selected action', async () => { "saved_objects": Array [ Object { "id": "1", + "rel": "primary", "type": "alert", }, Object { diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index 5c3e36b88879d..16fadc8b06cd5 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -9,7 +9,7 @@ import { AlertAction, State, Context, AlertType } from '../types'; import { Logger } from '../../../../../src/core/server'; import { transformActionParams } from './transform_action_params'; import { PluginStartContract as ActionsPluginStartContract } from '../../../../plugins/actions/server'; -import { IEventLogger, IEvent } from '../../../event_log/server'; +import { IEventLogger, IEvent, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { EVENT_LOG_ACTIONS } from '../plugin'; interface CreateExecutionHandlerOptions { @@ -96,7 +96,7 @@ export function createExecutionHandler({ instance_id: alertInstanceId, }, saved_objects: [ - { type: 'alert', id: alertId, ...namespace }, + { rel: SAVED_OBJECT_REL_PRIMARY, type: 'alert', id: alertId, ...namespace }, { type: 'action', id: action.id, ...namespace }, ], }, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 26d8a1d1777c0..35a0018049c33 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -172,6 +172,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -234,6 +235,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -254,6 +256,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -274,6 +277,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, Object { @@ -351,6 +355,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -371,6 +376,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -568,6 +574,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 26970dc6b2b0d..bf005301adc07 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -25,7 +25,7 @@ import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/ import { taskInstanceToAlertTaskInstance } from './alert_task_instance'; import { AlertInstances } from '../alert_instance/alert_instance'; import { EVENT_LOG_ACTIONS } from '../plugin'; -import { IEvent, IEventLogger } from '../../../event_log/server'; +import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error'; const FALLBACK_RETRY_INTERVAL: IntervalSchedule = { interval: '5m' }; @@ -174,7 +174,16 @@ export class TaskRunner { const alertLabel = `${this.alertType.id}:${alertId}: '${name}'`; const event: IEvent = { event: { action: EVENT_LOG_ACTIONS.execute }, - kibana: { saved_objects: [{ type: 'alert', id: alertId, namespace }] }, + kibana: { + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: 'alert', + id: alertId, + namespace, + }, + ], + }, }; eventLogger.startTiming(event); @@ -393,7 +402,14 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst alerting: { instance_id: id, }, - saved_objects: [{ type: 'alert', id: params.alertId, namespace: params.namespace }], + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: 'alert', + id: params.alertId, + namespace: params.namespace, + }, + ], }, message, }; diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index 61fbb779986dc..e40fc3e386bc8 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -307,7 +307,7 @@ export class EndpointDocGenerator { process: { entity_id: options.entityID ? options.entityID : this.randomString(10), parent: options.parentEntityID ? { entity_id: options.parentEntityID } : undefined, - name: options.processName ? options.processName : 'powershell.exe', + name: options.processName ? options.processName : randomProcessName(), }, }; } @@ -645,3 +645,16 @@ export class EndpointDocGenerator { return uuid.v4({ random: [...this.randomNGenerator(255, 16)] }); } } + +const fakeProcessNames = [ + 'lsass.exe', + 'notepad.exe', + 'mimikatz.exe', + 'powershell.exe', + 'iexlorer.exe', + 'explorer.exe', +]; +/** Return a random fake process name */ +function randomProcessName(): string { + return fakeProcessNames[Math.floor(Math.random() * fakeProcessNames.length)]; +} diff --git a/x-pack/plugins/endpoint/common/models/event.ts b/x-pack/plugins/endpoint/common/models/event.ts index 650486f3c3858..47f39d2d11797 100644 --- a/x-pack/plugins/endpoint/common/models/event.ts +++ b/x-pack/plugins/endpoint/common/models/event.ts @@ -4,17 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EndpointEvent, LegacyEndpointEvent } from '../types'; +import { LegacyEndpointEvent, ResolverEvent } from '../types'; -export function isLegacyEvent( - event: EndpointEvent | LegacyEndpointEvent -): event is LegacyEndpointEvent { +export function isLegacyEvent(event: ResolverEvent): event is LegacyEndpointEvent { return (event as LegacyEndpointEvent).endgame !== undefined; } -export function eventTimestamp( - event: EndpointEvent | LegacyEndpointEvent -): string | undefined | number { +export function eventTimestamp(event: ResolverEvent): string | undefined | number { if (isLegacyEvent(event)) { return event.endgame.timestamp_utc; } else { @@ -22,10 +18,31 @@ export function eventTimestamp( } } -export function eventName(event: EndpointEvent | LegacyEndpointEvent): string { +export function eventName(event: ResolverEvent): string { if (isLegacyEvent(event)) { return event.endgame.process_name ? event.endgame.process_name : ''; } else { return event.process.name; } } + +export function eventId(event: ResolverEvent): string { + if (isLegacyEvent(event)) { + return event.endgame.serial_event_id ? String(event.endgame.serial_event_id) : ''; + } + return event.event.id; +} + +export function entityId(event: ResolverEvent): string { + if (isLegacyEvent(event)) { + return event.endgame.unique_pid ? String(event.endgame.unique_pid) : ''; + } + return event.process.entity_id; +} + +export function parentEntityId(event: ResolverEvent): string | undefined { + if (isLegacyEvent(event)) { + return event.endgame.unique_ppid ? String(event.endgame.unique_ppid) : undefined; + } + return event.process.parent?.entity_id; +} diff --git a/x-pack/plugins/endpoint/common/schema/resolver.ts b/x-pack/plugins/endpoint/common/schema/resolver.ts new file mode 100644 index 0000000000000..f21307e407fd0 --- /dev/null +++ b/x-pack/plugins/endpoint/common/schema/resolver.ts @@ -0,0 +1,59 @@ +/* + * 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 } from '@kbn/config-schema'; + +/** + * Used to validate GET requests for a complete resolver tree. + */ +export const validateTree = { + params: schema.object({ id: schema.string() }), + query: schema.object({ + children: schema.number({ defaultValue: 10, min: 0, max: 100 }), + generations: schema.number({ defaultValue: 3, min: 0, max: 3 }), + ancestors: schema.number({ defaultValue: 3, min: 0, max: 5 }), + events: schema.number({ defaultValue: 100, min: 0, max: 1000 }), + afterEvent: schema.maybe(schema.string()), + afterChild: schema.maybe(schema.string()), + legacyEndpointID: schema.maybe(schema.string()), + }), +}; + +/** + * Used to validate GET requests for non process events for a specific event. + */ +export const validateEvents = { + params: schema.object({ id: schema.string() }), + query: schema.object({ + events: schema.number({ defaultValue: 100, min: 1, max: 1000 }), + afterEvent: schema.maybe(schema.string()), + legacyEndpointID: schema.maybe(schema.string()), + }), +}; + +/** + * Used to validate GET requests for the ancestors of a process event. + */ +export const validateAncestry = { + params: schema.object({ id: schema.string() }), + query: schema.object({ + ancestors: schema.number({ defaultValue: 0, min: 0, max: 10 }), + legacyEndpointID: schema.maybe(schema.string()), + }), +}; + +/** + * Used to validate GET requests for children of a specified process event. + */ +export const validateChildren = { + params: schema.object({ id: schema.string() }), + query: schema.object({ + children: schema.number({ defaultValue: 10, min: 10, max: 100 }), + generations: schema.number({ defaultValue: 3, min: 0, max: 3 }), + afterChild: schema.maybe(schema.string()), + legacyEndpointID: schema.maybe(schema.string()), + }), +}; diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 8773c5441860a..8fce15d1c794c 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -35,6 +35,31 @@ type ImmutableObject = { readonly [K in keyof T]: Immutable }; */ export type AlertAPIOrdering = 'asc' | 'desc'; +export interface ResolverNodeStats { + totalEvents: number; + totalAlerts: number; +} + +export interface ResolverNodePagination { + nextChild?: string | null; + nextEvent?: string | null; + nextAncestor?: string | null; + nextAlert?: string | null; +} + +/** + * A node that contains pointers to other nodes, arrrays of resolver events, and any metadata associated with resolver specific data + */ +export interface ResolverNode { + id: string; + children: ResolverNode[]; + events: ResolverEvent[]; + lifecycle: ResolverEvent[]; + ancestors?: ResolverNode[]; + pagination: ResolverNodePagination; + stats?: ResolverNodeStats; +} + /** * Returned by 'api/endpoint/alerts' */ diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts index d1b9a2cde4b31..bcfd6b96c9eb8 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts @@ -70,7 +70,6 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = core type: 'serverReturnedHostDetails', payload: response, }); - // FIXME: once we have the API implementation in place, we should call it parallel with the above api call and then dispatch this with the results of the second call dispatch({ type: 'serverReturnedHostPolicyResponse', payload: { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx index d18bc59a35f52..d32ad4dd9defc 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx @@ -33,5 +33,6 @@ export const AlertDetailResolver = styled( width: 100%; display: flex; flex-grow: 1; - min-height: 500px; + /* gross demo hack */ + min-height: calc(100vh - 505px); `; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts index 373afa89921dc..3ec15f2f1985d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts @@ -8,13 +8,11 @@ import { ResolverEvent } from '../../../../../common/types'; interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; - readonly payload: { - readonly data: { - readonly result: { - readonly search_results: readonly ResolverEvent[]; - }; - }; - }; + readonly payload: ResolverEvent[]; } -export type DataAction = ServerReturnedResolverData; +interface ServerFailedToReturnResolverData { + readonly type: 'serverFailedToReturnResolverData'; +} + +export type DataAction = ServerReturnedResolverData | ServerFailedToReturnResolverData; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts index f01136fe20ebf..f95ecc63d2a66 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts @@ -8,7 +8,7 @@ import { Store, createStore } from 'redux'; import { DataAction } from './action'; import { dataReducer } from './reducer'; import { DataState } from '../../types'; -import { LegacyEndpointEvent } from '../../../../../common/types'; +import { LegacyEndpointEvent, ResolverEvent } from '../../../../../common/types'; import { graphableProcesses, processNodePositionsAndEdgeLineSegments } from './selectors'; import { mockProcessEvent } from '../../models/process_event_test_helpers'; @@ -113,13 +113,7 @@ describe('resolver graph layout', () => { }); describe('when rendering no nodes', () => { beforeEach(() => { - const payload = { - data: { - result: { - search_results: [], - }, - }, - }; + const payload: ResolverEvent[] = []; const action: DataAction = { type: 'serverReturnedResolverData', payload }; store.dispatch(action); }); @@ -133,13 +127,7 @@ describe('resolver graph layout', () => { }); describe('when rendering one node', () => { beforeEach(() => { - const payload = { - data: { - result: { - search_results: [processA], - }, - }, - }; + const payload = [processA]; const action: DataAction = { type: 'serverReturnedResolverData', payload }; store.dispatch(action); }); @@ -153,13 +141,7 @@ describe('resolver graph layout', () => { }); describe('when rendering two nodes, one being the parent of the other', () => { beforeEach(() => { - const payload = { - data: { - result: { - search_results: [processA, processB], - }, - }, - }; + const payload = [processA, processB]; const action: DataAction = { type: 'serverReturnedResolverData', payload }; store.dispatch(action); }); @@ -173,23 +155,17 @@ describe('resolver graph layout', () => { }); describe('when rendering two forks, and one fork has an extra long tine', () => { beforeEach(() => { - const payload = { - data: { - result: { - search_results: [ - processA, - processB, - processC, - processD, - processE, - processF, - processG, - processH, - processI, - ], - }, - }, - }; + const payload = [ + processA, + processB, + processC, + processD, + processE, + processF, + processG, + processH, + processI, + ]; const action: DataAction = { type: 'serverReturnedResolverData', payload }; store.dispatch(action); }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts index a3184389a794e..fc307002819a9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts @@ -11,25 +11,28 @@ function initialState(): DataState { return { results: [], isLoading: false, + hasError: false, }; } export const dataReducer: Reducer = (state = initialState(), action) => { if (action.type === 'serverReturnedResolverData') { - const { - data: { - result: { search_results }, - }, - } = action.payload; return { ...state, - results: search_results, + results: action.payload, isLoading: false, + hasError: false, }; } else if (action.type === 'appRequestedResolverData') { return { ...state, isLoading: true, + hasError: false, + }; + } else if (action.type === 'serverFailedToReturnResolverData') { + return { + ...state, + hasError: true, }; } else { return state; 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 5dda54d4ed029..59ee4b3b87505 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 @@ -34,6 +34,10 @@ export function isLoading(state: DataState) { return state.isLoading; } +export function hasError(state: DataState) { + return state.hasError; +} + /** * An isometric projection is a method for representing three dimensional objects in 2 dimensions. * More information about isometric projections can be found here https://en.wikipedia.org/wiki/Isometric_projection. @@ -293,7 +297,7 @@ function* levelOrderWithWidths( metadata.firstChildWidth = width; } else { const firstChildWidth = widths.get(siblings[0]); - const lastChildWidth = widths.get(siblings[0]); + const lastChildWidth = widths.get(siblings[siblings.length - 1]); if (firstChildWidth === undefined || lastChildWidth === undefined) { /** * All widths have been precalcluated, so this will not happen. diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts index 4e57212e5c0c2..c7177c6387e7a 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts @@ -8,7 +8,7 @@ import { Dispatch, MiddlewareAPI } from 'redux'; import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public'; import { EndpointPluginServices } from '../../../plugin'; import { ResolverState, ResolverAction } from '../types'; -import { ResolverEvent } from '../../../../common/types'; +import { ResolverEvent, ResolverNode } from '../../../../common/types'; import * as event from '../../../../common/models/event'; type MiddlewareFactory = ( @@ -16,18 +16,18 @@ type MiddlewareFactory = ( ) => ( api: MiddlewareAPI, S> ) => (next: Dispatch) => (action: ResolverAction) => unknown; -interface Lifecycle { - lifecycle: ResolverEvent[]; -} -type ChildResponse = [Lifecycle]; -function flattenEvents(events: ChildResponse): ResolverEvent[] { - return events - .map((child: Lifecycle) => child.lifecycle) - .reduce( - (accumulator: ResolverEvent[], value: ResolverEvent[]) => accumulator.concat(value), - [] - ); +function flattenEvents(children: ResolverNode[], events: ResolverEvent[] = []): ResolverEvent[] { + return children.reduce((flattenedEvents, currentNode) => { + if (currentNode.lifecycle && currentNode.lifecycle.length > 0) { + flattenedEvents.push(...currentNode.lifecycle); + } + if (currentNode.children && currentNode.children.length > 0) { + return flattenEvents(currentNode.children, events); + } else { + return flattenedEvents; + } + }, events); } export const resolverMiddlewareFactory: MiddlewareFactory = context => { @@ -39,53 +39,43 @@ export const resolverMiddlewareFactory: MiddlewareFactory = context => { */ if (context?.services.http && action.payload.selectedEvent) { api.dispatch({ type: 'appRequestedResolverData' }); - let response = []; - let lifecycle: ResolverEvent[]; - let childEvents: ResolverEvent[]; - let relatedEvents: ResolverEvent[]; - let children = []; - const ancestors: ResolverEvent[] = []; - const maxAncestors = 5; - if (event.isLegacyEvent(action.payload.selectedEvent)) { - const uniquePid = action.payload.selectedEvent?.endgame?.unique_pid; - const legacyEndpointID = action.payload.selectedEvent?.agent?.id; - [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ - context.services.http.get(`/api/endpoint/resolver/${uniquePid}`, { - query: { legacyEndpointID }, - }), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`, { - query: { legacyEndpointID }, - }), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`, { - query: { legacyEndpointID }, - }), - ]); - childEvents = children.length > 0 ? flattenEvents(children) : []; - } else { - const uniquePid = action.payload.selectedEvent.process.entity_id; - const ppid = action.payload.selectedEvent.process.parent?.entity_id; - async function getAncestors(pid: string | undefined) { - if (ancestors.length < maxAncestors && pid !== undefined) { - const parent = await context?.services.http.get(`/api/endpoint/resolver/${pid}`); - ancestors.push(parent.lifecycle[0]); - if (parent.lifecycle[0].process?.parent?.entity_id) { - await getAncestors(parent.lifecycle[0].process.parent.entity_id); - } - } + try { + let lifecycle: ResolverEvent[]; + let children: ResolverNode[]; + let ancestors: ResolverNode[]; + if (event.isLegacyEvent(action.payload.selectedEvent)) { + const entityId = action.payload.selectedEvent?.endgame?.unique_pid; + const legacyEndpointID = action.payload.selectedEvent?.agent?.id; + [{ lifecycle, children, ancestors }] = await Promise.all([ + context.services.http.get(`/api/endpoint/resolver/${entityId}`, { + query: { legacyEndpointID, children: 5, ancestors: 5 }, + }), + ]); + } else { + const entityId = action.payload.selectedEvent.process.entity_id; + [{ lifecycle, children, ancestors }] = await Promise.all([ + context.services.http.get(`/api/endpoint/resolver/${entityId}`, { + query: { + children: 5, + ancestors: 5, + }, + }), + ]); } - [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ - context.services.http.get(`/api/endpoint/resolver/${uniquePid}`), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`), - getAncestors(ppid), - ]); + const response: ResolverEvent[] = [ + ...lifecycle, + ...flattenEvents(children), + ...flattenEvents(ancestors), + ]; + api.dispatch({ + type: 'serverReturnedResolverData', + payload: response, + }); + } catch (error) { + api.dispatch({ + type: 'serverFailedToReturnResolverData', + }); } - childEvents = children.length > 0 ? flattenEvents(children) : []; - response = [...lifecycle, ...childEvents, ...relatedEvents, ...ancestors]; - api.dispatch({ - type: 'serverReturnedResolverData', - payload: { data: { result: { search_results: response } } }, - }); } } }; 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 e8ae3d08e5cb6..7d09d90881da9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -102,6 +102,11 @@ function uiStateSelector(state: ResolverState) { */ export const isLoading = composeSelectors(dataStateSelector, dataSelectors.isLoading); +/** + * Whether or not the resolver encountered an error while fetching data + */ +export const hasError = composeSelectors(dataStateSelector, dataSelectors.hasError); + /** * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a * concern-specific selector. `selector` should return the concern-specific state. diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index d370bda0d1842..17aa598720c59 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -136,6 +136,7 @@ export type CameraState = { export interface DataState { readonly results: readonly ResolverEvent[]; isLoading: boolean; + hasError: boolean; } export type Vector2 = readonly [number, number]; 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 36155ece57a9c..2e7ca65c92dc1 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -8,6 +8,7 @@ import React, { useLayoutEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import styled from 'styled-components'; import { EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import * as selectors from '../store/selectors'; import { EdgeLine } from './edge_line'; import { Panel } from './panel'; @@ -59,6 +60,7 @@ export const Resolver = styled( const { projectionMatrix, ref, onMouseDown } = useCamera(); const isLoading = useSelector(selectors.isLoading); + const hasError = useSelector(selectors.hasError); const activeDescendantId = useSelector(selectors.uiActiveDescendantId); useLayoutEffect(() => { @@ -74,6 +76,16 @@ export const Resolver = styled(

+ ) : hasError ? ( +
+
+ {' '} + +
+
) : ( { } const serverResponseAction: ResolverAction = { type: 'serverReturnedResolverData', - payload: { - data: { - result: { - search_results: events, - }, - }, - }, + payload: events, }; act(() => { store.dispatch(serverResponseAction); diff --git a/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts b/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts index 92bd07a813d26..114251820ce4b 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts @@ -60,8 +60,6 @@ export const getRequestData = async ( reqData.fromIndex = reqData.pageIndex * reqData.pageSize; } - // See: https://github.com/elastic/elasticsearch-js/issues/662 - // and https://github.com/elastic/endpoint-app-team/issues/221 if ( reqData.searchBefore !== undefined && reqData.searchBefore[0] === '' && diff --git a/x-pack/plugins/endpoint/server/routes/metadata/index.ts b/x-pack/plugins/endpoint/server/routes/metadata/index.ts index 99dc4ac9f9e33..08950930441df 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/index.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/index.ts @@ -171,7 +171,6 @@ async function enrichHostMetadata( try { /** * Get agent status by elastic agent id if available or use the host id. - * https://github.com/elastic/endpoint-app-team/issues/354 */ if (!elasticAgentId) { diff --git a/x-pack/plugins/endpoint/server/routes/resolver.ts b/x-pack/plugins/endpoint/server/routes/resolver.ts index a96d431225b15..3599acacb4f59 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver.ts @@ -6,20 +6,27 @@ import { IRouter } from 'kibana/server'; import { EndpointAppContext } from '../types'; -import { handleRelatedEvents, validateRelatedEvents } from './resolver/related_events'; -import { handleChildren, validateChildren } from './resolver/children'; -import { handleLifecycle, validateLifecycle } from './resolver/lifecycle'; +import { + validateTree, + validateEvents, + validateChildren, + validateAncestry, +} from '../../common/schema/resolver'; +import { handleEvents } from './resolver/events'; +import { handleChildren } from './resolver/children'; +import { handleAncestry } from './resolver/ancestry'; +import { handleTree } from './resolver/tree'; export function registerResolverRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { const log = endpointAppContext.logFactory.get('resolver'); router.get( { - path: '/api/endpoint/resolver/{id}/related', - validate: validateRelatedEvents, + path: '/api/endpoint/resolver/{id}/events', + validate: validateEvents, options: { authRequired: true }, }, - handleRelatedEvents(log, endpointAppContext) + handleEvents(log, endpointAppContext) ); router.get( @@ -31,12 +38,21 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp handleChildren(log, endpointAppContext) ); + router.get( + { + path: '/api/endpoint/resolver/{id}/ancestry', + validate: validateAncestry, + options: { authRequired: true }, + }, + handleAncestry(log, endpointAppContext) + ); + router.get( { path: '/api/endpoint/resolver/{id}', - validate: validateLifecycle, + validate: validateTree, options: { authRequired: true }, }, - handleLifecycle(log, endpointAppContext) + handleTree(log, endpointAppContext) ); } diff --git a/x-pack/plugins/endpoint/server/routes/resolver/ancestry.ts b/x-pack/plugins/endpoint/server/routes/resolver/ancestry.ts new file mode 100644 index 0000000000000..6648dc5b9e493 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/ancestry.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 { RequestHandler, Logger } from 'kibana/server'; +import { TypeOf } from '@kbn/config-schema'; +import { validateAncestry } from '../../../common/schema/resolver'; +import { Fetcher } from './utils/fetch'; +import { EndpointAppContext } from '../../types'; + +export function handleAncestry( + log: Logger, + endpointAppContext: EndpointAppContext +): RequestHandler, TypeOf> { + return async (context, req, res) => { + const { + params: { id }, + query: { ancestors, legacyEndpointID: endpointID }, + } = req; + try { + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); + + const client = context.core.elasticsearch.dataClient; + const indexPattern = await indexRetriever.getEventIndexPattern(context); + + const fetcher = new Fetcher(client, id, indexPattern, endpointID); + const tree = await fetcher.ancestors(ancestors + 1); + + return res.ok({ + body: tree.render(), + }); + } catch (err) { + log.warn(err); + return res.internalError({ body: err }); + } + }; +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/children.ts b/x-pack/plugins/endpoint/server/routes/resolver/children.ts index c3b19b6c912b6..bb18b29a4b947 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/children.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/children.ts @@ -4,87 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; -import { schema } from '@kbn/config-schema'; import { RequestHandler, Logger } from 'kibana/server'; -import { extractEntityID } from './utils/normalize'; -import { getPaginationParams } from './utils/pagination'; -import { LifecycleQuery } from './queries/lifecycle'; -import { ChildrenQuery } from './queries/children'; +import { TypeOf } from '@kbn/config-schema'; +import { validateChildren } from '../../../common/schema/resolver'; +import { Fetcher } from './utils/fetch'; import { EndpointAppContext } from '../../types'; -interface ChildrenQueryParams { - after?: string; - limit: number; - /** - * legacyEndpointID is optional because there are two different types of identifiers: - * - * Legacy - * A legacy Entity ID is made up of the agent.id and unique_pid fields. The client will need to identify if - * it's looking at a legacy event and use those fields when making requests to the backend. The - * request would be /resolver/{id}?legacyEndpointID=and the {id} would be the unique_pid. - * - * Elastic Endpoint - * When interacting with the new form of data the client doesn't need the legacyEndpointID because it's already a - * part of the entityID in the new type of event. So for the same request the client would just hit resolver/{id} - * and the {id} would be entityID stored in the event's process.entity_id field. - */ - legacyEndpointID?: string; -} - -interface ChildrenPathParams { - id: string; -} - -export const validateChildren = { - params: schema.object({ id: schema.string() }), - query: schema.object({ - after: schema.maybe(schema.string()), - limit: schema.number({ defaultValue: 10, min: 1, max: 100 }), - legacyEndpointID: schema.maybe(schema.string()), - }), -}; - export function handleChildren( log: Logger, endpointAppContext: EndpointAppContext -): RequestHandler { +): RequestHandler, TypeOf> { return async (context, req, res) => { const { params: { id }, - query: { limit, after, legacyEndpointID }, + query: { children, generations, afterChild, legacyEndpointID: endpointID }, } = req; try { const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); - const pagination = getPaginationParams(limit, after); const indexPattern = await indexRetriever.getEventIndexPattern(context); const client = context.core.elasticsearch.dataClient; - const childrenQuery = new ChildrenQuery(indexPattern, legacyEndpointID, pagination); - const lifecycleQuery = new LifecycleQuery(indexPattern, legacyEndpointID); - - // Retrieve the related child process events for a given process - const { total, results: events, nextCursor } = await childrenQuery.search(client, id); - const childIDs = events.map(extractEntityID); - - // Retrieve the lifecycle events for the child processes (e.g. started, terminated etc) - // this needs to fire after the above since we don't yet have the entity ids until we - // run the first query - const { results: lifecycleEvents } = await lifecycleQuery.search(client, ...childIDs); - // group all of the lifecycle events by the child process id - const lifecycleGroups = Object.values(_.groupBy(lifecycleEvents, extractEntityID)); - const children = lifecycleGroups.map(group => ({ lifecycle: group })); + const fetcher = new Fetcher(client, id, indexPattern, endpointID); + const tree = await fetcher.children(children, generations, afterChild); return res.ok({ - body: { - children, - pagination: { - total, - next: nextCursor, - limit, - }, - }, + body: tree.render(), }); } catch (err) { log.warn(err); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/events.ts b/x-pack/plugins/endpoint/server/routes/resolver/events.ts new file mode 100644 index 0000000000000..a70a6e8d097d0 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/events.ts @@ -0,0 +1,38 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import { RequestHandler, Logger } from 'kibana/server'; +import { validateEvents } from '../../../common/schema/resolver'; +import { Fetcher } from './utils/fetch'; +import { EndpointAppContext } from '../../types'; + +export function handleEvents( + log: Logger, + endpointAppContext: EndpointAppContext +): RequestHandler, TypeOf> { + return async (context, req, res) => { + const { + params: { id }, + query: { events, afterEvent, legacyEndpointID: endpointID }, + } = req; + try { + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); + const client = context.core.elasticsearch.dataClient; + const indexPattern = await indexRetriever.getEventIndexPattern(context); + + const fetcher = new Fetcher(client, id, indexPattern, endpointID); + const tree = await fetcher.events(events, afterEvent); + + return res.ok({ + body: tree.render(), + }); + } catch (err) { + log.warn(err); + return res.internalError({ body: err }); + } + }; +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts b/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts deleted file mode 100644 index 91a4c5d49bc54..0000000000000 --- a/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts +++ /dev/null @@ -1,96 +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 { schema } from '@kbn/config-schema'; -import { RequestHandler, Logger } from 'kibana/server'; -import { extractParentEntityID } from './utils/normalize'; -import { LifecycleQuery } from './queries/lifecycle'; -import { ResolverEvent } from '../../../common/types'; -import { EndpointAppContext } from '../../types'; - -interface LifecycleQueryParams { - ancestors: number; - /** - * legacyEndpointID is optional because there are two different types of identifiers: - * - * Legacy - * A legacy Entity ID is made up of the agent.id and unique_pid fields. The client will need to identify if - * it's looking at a legacy event and use those fields when making requests to the backend. The - * request would be /resolver/{id}?legacyEndpointID=and the {id} would be the unique_pid. - * - * Elastic Endpoint - * When interacting with the new form of data the client doesn't need the legacyEndpointID because it's already a - * part of the entityID in the new type of event. So for the same request the client would just hit resolver/{id} - * and the {id} would be entityID stored in the event's process.entity_id field. - */ - legacyEndpointID?: string; -} - -interface LifecyclePathParams { - id: string; -} - -export const validateLifecycle = { - params: schema.object({ id: schema.string() }), - query: schema.object({ - ancestors: schema.number({ defaultValue: 0, min: 0, max: 10 }), - legacyEndpointID: schema.maybe(schema.string()), - }), -}; - -function getParentEntityID(results: ResolverEvent[]) { - return results.length === 0 ? undefined : extractParentEntityID(results[0]); -} - -export function handleLifecycle( - log: Logger, - endpointAppContext: EndpointAppContext -): RequestHandler { - return async (context, req, res) => { - const { - params: { id }, - query: { ancestors, legacyEndpointID }, - } = req; - try { - const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); - const ancestorLifecycles = []; - const client = context.core.elasticsearch.dataClient; - const indexPattern = await indexRetriever.getEventIndexPattern(context); - const lifecycleQuery = new LifecycleQuery(indexPattern, legacyEndpointID); - const { results: processLifecycle } = await lifecycleQuery.search(client, id); - let nextParentID = getParentEntityID(processLifecycle); - - if (nextParentID) { - for (let i = 0; i < ancestors; i++) { - const { results: lifecycle } = await lifecycleQuery.search(client, nextParentID); - nextParentID = getParentEntityID(lifecycle); - - if (!nextParentID) { - break; - } - - ancestorLifecycles.push({ - lifecycle, - }); - } - } - - return res.ok({ - body: { - lifecycle: processLifecycle, - ancestors: ancestorLifecycles, - pagination: { - next: nextParentID || null, - ancestors, - }, - }, - }); - } catch (err) { - log.warn(err); - return res.internalError({ body: err }); - } - }; -} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts index 3b3b4b0c9e8ac..eba4e5581c136 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts @@ -4,8 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SearchResponse } from 'elasticsearch'; import { IScopedClusterClient } from 'kibana/server'; -import { paginate, paginatedResults, PaginationParams } from '../utils/pagination'; +import { ResolverEvent } from '../../../../common/types'; +import { + paginate, + paginatedResults, + PaginationParams, + PaginatedResults, +} from '../utils/pagination'; import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; import { legacyEventIndexPattern } from './legacy_event_index_pattern'; @@ -16,11 +23,13 @@ export abstract class ResolverQuery { private readonly pagination?: PaginationParams ) {} - protected paginateBy(field: string, query: JsonObject) { - if (!this.pagination) { - return query; - } - return paginate(this.pagination, field, query); + protected paginateBy(tiebreaker: string, aggregator: string) { + return (query: JsonObject) => { + if (!this.pagination) { + return query; + } + return paginate(this.pagination, tiebreaker, aggregator, query); + }; } build(...ids: string[]) { @@ -31,7 +40,11 @@ export abstract class ResolverQuery { } async search(client: IScopedClusterClient, ...ids: string[]) { - return paginatedResults(await client.callAsCurrentUser('search', this.build(...ids))); + return this.postSearch(await client.callAsCurrentUser('search', this.build(...ids))); + } + + protected postSearch(response: SearchResponse): PaginatedResults { + return paginatedResults(response); } protected abstract legacyQuery( diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts index d1d090579f0cd..2a097e87c38b2 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts @@ -32,15 +32,28 @@ describe('children events query', () => { term: { 'event.category': 'process' }, }, { - term: { 'event.type': 'process_start' }, + term: { 'event.kind': 'event' }, + }, + { + bool: { + should: [ + { + term: { 'event.type': 'process_start' }, + }, + { + term: { 'event.action': 'fork_event' }, + }, + ], + }, }, ], }, }, aggs: { - total: { - value_count: { - field: 'endgame.serial_event_id', + totals: { + terms: { + field: 'endgame.unique_ppid', + size: 1, }, }, }, @@ -67,20 +80,14 @@ describe('children events query', () => { bool: { filter: [ { - bool: { - should: [ - { - terms: { 'endpoint.process.parent.entity_id': ['baz'] }, - }, - { - terms: { 'process.parent.entity_id': ['baz'] }, - }, - ], - }, + terms: { 'process.parent.entity_id': ['baz'] }, }, { term: { 'event.category': 'process' }, }, + { + term: { 'event.kind': 'event' }, + }, { term: { 'event.type': 'start' }, }, @@ -88,9 +95,10 @@ describe('children events query', () => { }, }, aggs: { - total: { - value_count: { - field: 'event.id', + totals: { + terms: { + field: 'process.parent.entity_id', + size: 1, }, }, }, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.ts index 6d084a0cf20e5..690c926d7e6d6 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.ts @@ -7,8 +7,9 @@ import { ResolverQuery } from './base'; export class ChildrenQuery extends ResolverQuery { protected legacyQuery(endpointID: string, uniquePIDs: string[], index: string) { + const paginator = this.paginateBy('endgame.serial_event_id', 'endgame.unique_ppid'); return { - body: this.paginateBy('endgame.serial_event_id', { + body: paginator({ query: { bool: { filter: [ @@ -22,11 +23,19 @@ export class ChildrenQuery extends ResolverQuery { term: { 'event.category': 'process' }, }, { - // Corner case, we could only have a process_running or process_terminated - // so to solve this we'll probably want to either search for all of them and only return one if that's - // possible in elastic search or in memory pull out a single event to return - // https://github.com/elastic/endpoint-app-team/issues/168 - term: { 'event.type': 'process_start' }, + term: { 'event.kind': 'event' }, + }, + { + bool: { + should: [ + { + term: { 'event.type': 'process_start' }, + }, + { + term: { 'event.action': 'fork_event' }, + }, + ], + }, }, ], }, @@ -37,31 +46,22 @@ export class ChildrenQuery extends ResolverQuery { } protected query(entityIDs: string[], index: string) { + const paginator = this.paginateBy('event.id', 'process.parent.entity_id'); return { - body: this.paginateBy('event.id', { + body: paginator({ query: { bool: { filter: [ { - bool: { - should: [ - { - terms: { 'endpoint.process.parent.entity_id': entityIDs }, - }, - { - terms: { 'process.parent.entity_id': entityIDs }, - }, - ], - }, + terms: { 'process.parent.entity_id': entityIDs }, }, { term: { 'event.category': 'process' }, }, { - // Corner case, we could only have a process_running or process_terminated - // so to solve this we'll probably want to either search for all of them and only return one if that's - // possible in elastic search or in memory pull out a single event to return - // https://github.com/elastic/endpoint-app-team/issues/168 + term: { 'event.kind': 'event' }, + }, + { term: { 'event.type': 'start' }, }, ], diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/events.test.ts similarity index 75% rename from x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts rename to x-pack/plugins/endpoint/server/routes/resolver/queries/events.test.ts index 26205e9db6e45..78e5ee9226581 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/events.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { RelatedEventsQuery } from './related_events'; +import { EventsQuery } from './events'; import { fakeEventIndexPattern } from './children.test'; import { legacyEventIndexPattern } from './legacy_event_index_pattern'; @@ -11,7 +11,7 @@ describe('related events query', () => { it('generates the correct legacy queries', () => { const timestamp = new Date().getTime(); expect( - new RelatedEventsQuery(legacyEventIndexPattern, 'awesome-id', { + new EventsQuery(legacyEventIndexPattern, 'awesome-id', { size: 1, timestamp, eventID: 'foo', @@ -27,6 +27,9 @@ describe('related events query', () => { { term: { 'agent.id': 'awesome-id' }, }, + { + term: { 'event.kind': 'event' }, + }, { bool: { must_not: { @@ -38,9 +41,10 @@ describe('related events query', () => { }, }, aggs: { - total: { - value_count: { - field: 'endgame.serial_event_id', + totals: { + terms: { + field: 'endgame.unique_pid', + size: 1, }, }, }, @@ -56,7 +60,7 @@ describe('related events query', () => { const timestamp = new Date().getTime(); expect( - new RelatedEventsQuery(fakeEventIndexPattern, undefined, { + new EventsQuery(fakeEventIndexPattern, undefined, { size: 1, timestamp, eventID: 'bar', @@ -67,16 +71,10 @@ describe('related events query', () => { bool: { filter: [ { - bool: { - should: [ - { - terms: { 'endpoint.process.entity_id': ['baz'] }, - }, - { - terms: { 'process.entity_id': ['baz'] }, - }, - ], - }, + terms: { 'process.entity_id': ['baz'] }, + }, + { + term: { 'event.kind': 'event' }, }, { bool: { @@ -89,9 +87,10 @@ describe('related events query', () => { }, }, aggs: { - total: { - value_count: { - field: 'event.id', + totals: { + terms: { + field: 'process.entity_id', + size: 1, }, }, }, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/events.ts similarity index 74% rename from x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.ts rename to x-pack/plugins/endpoint/server/routes/resolver/queries/events.ts index cc5afe8face8d..b622cb8a21111 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/events.ts @@ -6,10 +6,11 @@ import { ResolverQuery } from './base'; import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; -export class RelatedEventsQuery extends ResolverQuery { +export class EventsQuery extends ResolverQuery { protected legacyQuery(endpointID: string, uniquePIDs: string[], index: string): JsonObject { + const paginator = this.paginateBy('endgame.serial_event_id', 'endgame.unique_pid'); return { - body: this.paginateBy('endgame.serial_event_id', { + body: paginator({ query: { bool: { filter: [ @@ -19,6 +20,9 @@ export class RelatedEventsQuery extends ResolverQuery { { term: { 'agent.id': endpointID }, }, + { + term: { 'event.kind': 'event' }, + }, { bool: { must_not: { @@ -35,22 +39,17 @@ export class RelatedEventsQuery extends ResolverQuery { } protected query(entityIDs: string[], index: string): JsonObject { + const paginator = this.paginateBy('event.id', 'process.entity_id'); return { - body: this.paginateBy('event.id', { + body: paginator({ query: { bool: { filter: [ { - bool: { - should: [ - { - terms: { 'endpoint.process.entity_id': entityIDs }, - }, - { - terms: { 'process.entity_id': entityIDs }, - }, - ], - }, + terms: { 'process.entity_id': entityIDs }, + }, + { + term: { 'event.kind': 'event' }, }, { bool: { diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts index 9d3c675ecc54c..296135af83b72 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts @@ -21,12 +21,16 @@ describe('lifecycle query', () => { { term: { 'agent.id': 'awesome-id' }, }, + { + term: { 'event.kind': 'event' }, + }, { term: { 'event.category': 'process' }, }, ], }, }, + size: 10000, sort: [{ '@timestamp': 'asc' }], }, index: legacyEventIndexPattern, @@ -40,16 +44,10 @@ describe('lifecycle query', () => { bool: { filter: [ { - bool: { - should: [ - { - terms: { 'endpoint.process.entity_id': ['baz'] }, - }, - { - terms: { 'process.entity_id': ['baz'] }, - }, - ], - }, + terms: { 'process.entity_id': ['baz'] }, + }, + { + term: { 'event.kind': 'event' }, }, { term: { 'event.category': 'process' }, @@ -57,6 +55,7 @@ describe('lifecycle query', () => { ], }, }, + size: 10000, sort: [{ '@timestamp': 'asc' }], }, index: fakeEventIndexPattern, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.ts index 290c601e0e9d8..e775b0cf9b6d2 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.ts @@ -6,7 +6,6 @@ import { ResolverQuery } from './base'; import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; -// consider limiting the response size to a reasonable value in case we have a bunch of lifecycle events export class LifecycleQuery extends ResolverQuery { protected legacyQuery(endpointID: string, uniquePIDs: string[], index: string): JsonObject { return { @@ -20,12 +19,16 @@ export class LifecycleQuery extends ResolverQuery { { term: { 'agent.id': endpointID }, }, + { + term: { 'event.kind': 'event' }, + }, { term: { 'event.category': 'process' }, }, ], }, }, + size: 10000, sort: [{ '@timestamp': 'asc' }], }, index, @@ -39,16 +42,10 @@ export class LifecycleQuery extends ResolverQuery { bool: { filter: [ { - bool: { - should: [ - { - terms: { 'endpoint.process.entity_id': entityIDs }, - }, - { - terms: { 'process.entity_id': entityIDs }, - }, - ], - }, + terms: { 'process.entity_id': entityIDs }, + }, + { + term: { 'event.kind': 'event' }, }, { term: { 'event.category': 'process' }, @@ -56,6 +53,7 @@ export class LifecycleQuery extends ResolverQuery { ], }, }, + size: 10000, sort: [{ '@timestamp': 'asc' }], }, index, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.test.ts new file mode 100644 index 0000000000000..17a158aec7cf5 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.test.ts @@ -0,0 +1,188 @@ +/* + * 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 { legacyEventIndexPattern } from './legacy_event_index_pattern'; +import { StatsQuery } from './stats'; +import { fakeEventIndexPattern } from './children.test'; + +describe('stats query', () => { + it('generates the correct legacy queries', () => { + expect(new StatsQuery(legacyEventIndexPattern, 'awesome-id').build('5')).toStrictEqual({ + body: { + size: 0, + query: { + bool: { + filter: [ + { + term: { + 'agent.id': 'awesome-id', + }, + }, + { + bool: { + should: [ + { + bool: { + filter: [ + { + term: { + 'event.kind': 'event', + }, + }, + { + terms: { + 'endgame.unique_pid': ['5'], + }, + }, + { + bool: { + must_not: { + term: { + 'event.category': 'process', + }, + }, + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + term: { + 'event.kind': 'alert', + }, + }, + { + terms: { + 'endgame.data.alert_details.acting_process.unique_pid': ['5'], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + alerts: { + filter: { + term: { + 'event.kind': 'alert', + }, + }, + aggs: { + ids: { + terms: { + field: 'endgame.data.alert_details.acting_process.unique_pid', + }, + }, + }, + }, + events: { + filter: { + term: { + 'event.kind': 'event', + }, + }, + aggs: { + ids: { + terms: { + field: 'endgame.unique_pid', + }, + }, + }, + }, + }, + }, + index: legacyEventIndexPattern, + }); + }); + + it('generates the correct non-legacy queries', () => { + expect(new StatsQuery(fakeEventIndexPattern).build('baz')).toStrictEqual({ + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + 'process.entity_id': ['baz'], + }, + }, + { + bool: { + should: [ + { + bool: { + filter: [ + { + term: { + 'event.kind': 'event', + }, + }, + { + bool: { + must_not: { + term: { + 'event.category': 'process', + }, + }, + }, + }, + ], + }, + }, + { + term: { + 'event.kind': 'alert', + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + alerts: { + filter: { + term: { + 'event.kind': 'alert', + }, + }, + aggs: { + ids: { + terms: { + field: 'process.entity_id', + }, + }, + }, + }, + events: { + filter: { + term: { + 'event.kind': 'event', + }, + }, + aggs: { + ids: { + terms: { + field: 'process.entity_id', + }, + }, + }, + }, + }, + }, + index: fakeEventIndexPattern, + }); + }); +}); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.ts new file mode 100644 index 0000000000000..7db3ab2b0cb1f --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.ts @@ -0,0 +1,147 @@ +/* + * 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 { SearchResponse } from 'elasticsearch'; +import { ResolverQuery } from './base'; +import { ResolverEvent } from '../../../../common/types'; +import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; +import { PaginatedResults } from '../utils/pagination'; + +export class StatsQuery extends ResolverQuery { + protected postSearch(response: SearchResponse): PaginatedResults { + const alerts = response.aggregations.alerts.ids.buckets.reduce( + (cummulative: any, bucket: any) => ({ ...cummulative, [bucket.key]: bucket.doc_count }), + {} + ); + const events = response.aggregations.events.ids.buckets.reduce( + (cummulative: any, bucket: any) => ({ ...cummulative, [bucket.key]: bucket.doc_count }), + {} + ); + return { + totals: {}, + results: [], + extras: { + alerts, + events, + }, + }; + } + + protected legacyQuery(endpointID: string, uniquePIDs: string[], index: string): JsonObject { + return { + body: { + size: 0, + query: { + bool: { + filter: [ + { + term: { 'agent.id': endpointID }, + }, + { + bool: { + should: [ + { + bool: { + filter: [ + { term: { 'event.kind': 'event' } }, + { terms: { 'endgame.unique_pid': uniquePIDs } }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { term: { 'event.kind': 'alert' } }, + { + terms: { + 'endgame.data.alert_details.acting_process.unique_pid': uniquePIDs, + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + alerts: { + filter: { term: { 'event.kind': 'alert' } }, + aggs: { + ids: { terms: { field: 'endgame.data.alert_details.acting_process.unique_pid' } }, + }, + }, + events: { + filter: { term: { 'event.kind': 'event' } }, + aggs: { + ids: { terms: { field: 'endgame.unique_pid' } }, + }, + }, + }, + }, + index, + }; + } + + protected query(entityIDs: string[], index: string): JsonObject { + return { + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { 'process.entity_id': entityIDs } }, + { + bool: { + should: [ + { + bool: { + filter: [ + { term: { 'event.kind': 'event' } }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + { term: { 'event.kind': 'alert' } }, + ], + }, + }, + ], + }, + }, + aggs: { + alerts: { + filter: { term: { 'event.kind': 'alert' } }, + aggs: { + ids: { terms: { field: 'process.entity_id' } }, + }, + }, + events: { + filter: { term: { 'event.kind': 'event' } }, + aggs: { + ids: { terms: { field: 'process.entity_id' } }, + }, + }, + }, + }, + index, + }; + } +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts b/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts deleted file mode 100644 index 83e111a1e62e6..0000000000000 --- a/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts +++ /dev/null @@ -1,77 +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 { schema } from '@kbn/config-schema'; -import { RequestHandler, Logger } from 'kibana/server'; -import { getPaginationParams } from './utils/pagination'; -import { RelatedEventsQuery } from './queries/related_events'; -import { EndpointAppContext } from '../../types'; - -interface RelatedEventsQueryParams { - after?: string; - limit: number; - /** - * legacyEndpointID is optional because there are two different types of identifiers: - * - * Legacy - * A legacy Entity ID is made up of the agent.id and unique_pid fields. The client will need to identify if - * it's looking at a legacy event and use those fields when making requests to the backend. The - * request would be /resolver/{id}?legacyEndpointID=and the {id} would be the unique_pid. - * - * Elastic Endpoint - * When interacting with the new form of data the client doesn't need the legacyEndpointID because it's already a - * part of the entityID in the new type of event. So for the same request the client would just hit resolver/{id} - * and the {id} would be entityID stored in the event's process.entity_id field. - */ - legacyEndpointID?: string; -} - -interface RelatedEventsPathParams { - id: string; -} - -export const validateRelatedEvents = { - params: schema.object({ id: schema.string() }), - query: schema.object({ - after: schema.maybe(schema.string()), - limit: schema.number({ defaultValue: 100, min: 1, max: 1000 }), - legacyEndpointID: schema.maybe(schema.string()), - }), -}; - -export function handleRelatedEvents( - log: Logger, - endpointAppContext: EndpointAppContext -): RequestHandler { - return async (context, req, res) => { - const { - params: { id }, - query: { limit, after, legacyEndpointID }, - } = req; - try { - const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); - const pagination = getPaginationParams(limit, after); - - const client = context.core.elasticsearch.dataClient; - const indexPattern = await indexRetriever.getEventIndexPattern(context); - // Retrieve the related non-process events for a given process - const relatedEventsQuery = new RelatedEventsQuery(indexPattern, legacyEndpointID, pagination); - const relatedEvents = await relatedEventsQuery.search(client, id); - - const { total, results: events, nextCursor } = relatedEvents; - - return res.ok({ - body: { - events, - pagination: { total, next: nextCursor, limit }, - }, - }); - } catch (err) { - log.warn(err); - return res.internalError({ body: err }); - } - }; -} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/tree.ts b/x-pack/plugins/endpoint/server/routes/resolver/tree.ts new file mode 100644 index 0000000000000..25f15586341d5 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/tree.ts @@ -0,0 +1,53 @@ +/* + * 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 { RequestHandler, Logger } from 'kibana/server'; +import { TypeOf } from '@kbn/config-schema'; +import { validateTree } from '../../../common/schema/resolver'; +import { Fetcher } from './utils/fetch'; +import { Tree } from './utils/tree'; +import { EndpointAppContext } from '../../types'; + +export function handleTree( + log: Logger, + endpointAppContext: EndpointAppContext +): RequestHandler, TypeOf> { + return async (context, req, res) => { + const { + params: { id }, + query: { + children, + generations, + ancestors, + events, + afterEvent, + afterChild, + legacyEndpointID: endpointID, + }, + } = req; + try { + const client = context.core.elasticsearch.dataClient; + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); + const indexPattern = await indexRetriever.getEventIndexPattern(context); + + const fetcher = new Fetcher(client, id, indexPattern, endpointID); + const tree = await Tree.merge( + fetcher.children(children, generations, afterChild), + fetcher.ancestors(ancestors + 1), + fetcher.events(events, afterEvent) + ); + + const enrichedTree = await fetcher.stats(tree); + + return res.ok({ + body: enrichedTree.render(), + }); + } catch (err) { + log.warn(err); + return res.internalError({ body: 'Error retrieving tree.' }); + } + }; +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/fetch.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/fetch.ts new file mode 100644 index 0000000000000..7315b4ee6c618 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/utils/fetch.ts @@ -0,0 +1,123 @@ +/* + * 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 { IScopedClusterClient } from 'kibana/server'; +import { entityId, parentEntityId } from '../../../../common/models/event'; +import { getPaginationParams } from './pagination'; +import { Tree } from './tree'; +import { LifecycleQuery } from '../queries/lifecycle'; +import { ChildrenQuery } from '../queries/children'; +import { EventsQuery } from '../queries/events'; +import { StatsQuery } from '../queries/stats'; + +export class Fetcher { + constructor( + private readonly client: IScopedClusterClient, + private readonly id: string, + private readonly indexPattern: string, + private readonly endpointID?: string + ) {} + + public async ancestors(limit: number): Promise { + const tree = new Tree(this.id); + await this.doAncestors(tree, this.id, this.id, limit); + return tree; + } + + public async children(limit: number, generations: number, after?: string): Promise { + const tree = new Tree(this.id); + await this.doChildren(tree, [this.id], limit, generations, after); + return tree; + } + + public async events(limit: number, after?: string): Promise { + const tree = new Tree(this.id); + await this.doEvents(tree, limit, after); + return tree; + } + + public async stats(tree: Tree): Promise { + await this.doStats(tree); + return tree; + } + + private async doAncestors(tree: Tree, curNode: string, previousNode: string, levels: number) { + if (levels === 0) { + tree.setNextAncestor(curNode); + return; + } + + const query = new LifecycleQuery(this.indexPattern, this.endpointID); + const { results } = await query.search(this.client, curNode); + + if (results.length === 0) { + tree.setNextAncestor(null); + return; + } + tree.addAncestor(previousNode, ...results); + + const next = parentEntityId(results[0]); + if (next !== undefined) { + await this.doAncestors(tree, next, curNode, levels - 1); + } + } + + private async doEvents(tree: Tree, limit: number, after?: string) { + const query = new EventsQuery( + this.indexPattern, + this.endpointID, + getPaginationParams(limit, after) + ); + + const { totals, results } = await query.search(this.client, this.id); + tree.addEvent(...results); + tree.paginateEvents(totals, results); + if (results.length === 0) tree.setNextEvent(null); + } + + private async doChildren( + tree: Tree, + ids: string[], + limit: number, + levels: number, + after?: string + ) { + if (levels === 0 || ids.length === 0) return; + + const childrenQuery = new ChildrenQuery( + this.indexPattern, + this.endpointID, + getPaginationParams(limit, after) + ); + const lifecycleQuery = new LifecycleQuery(this.indexPattern, this.endpointID); + + const { totals, results } = await childrenQuery.search(this.client, ...ids); + if (results.length === 0) { + tree.markLeafNode(...ids); + return; + } + + const childIDs = results.map(entityId); + const children = (await lifecycleQuery.search(this.client, ...childIDs)).results; + + tree.addChild(...children); + tree.paginateChildren(totals, results); + tree.markLeafNode(...childIDs); + + await this.doChildren(tree, childIDs, limit * limit, levels - 1); + } + + private async doStats(tree: Tree) { + const statsQuery = new StatsQuery(this.indexPattern, this.endpointID); + const ids = tree.ids(); + const { extras } = await statsQuery.search(this.client, ...ids); + const alerts = extras?.alerts || {}; + const events = extras?.events || {}; + ids.forEach(id => { + tree.addStats(id, { totalAlerts: alerts[id] || 0, totalEvents: events[id] || 0 }); + }); + } +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/normalize.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/normalize.ts deleted file mode 100644 index 6d5ac8efdc1da..0000000000000 --- a/x-pack/plugins/endpoint/server/routes/resolver/utils/normalize.ts +++ /dev/null @@ -1,30 +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 { ResolverEvent } from '../../../../common/types'; -import { isLegacyEvent } from '../../../../common/models/event'; - -export function extractEventID(event: ResolverEvent) { - if (isLegacyEvent(event)) { - return String(event.endgame.serial_event_id); - } - return event.event.id; -} - -export function extractEntityID(event: ResolverEvent) { - if (isLegacyEvent(event)) { - return String(event.endgame.unique_pid); - } - return event.process.entity_id; -} - -export function extractParentEntityID(event: ResolverEvent) { - if (isLegacyEvent(event)) { - const ppid = event.endgame.unique_ppid; - return ppid && String(ppid); // if unique_ppid is undefined return undefined - } - return event.process.parent?.entity_id; -} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts index 5a64f3ff9ddb6..20249b81660bb 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts @@ -6,7 +6,7 @@ import { SearchResponse } from 'elasticsearch'; import { ResolverEvent } from '../../../../common/types'; -import { extractEventID } from './normalize'; +import { entityId } from '../../../../common/models/event'; import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; export interface PaginationParams { @@ -15,12 +15,19 @@ export interface PaginationParams { eventID?: string; } +export interface PaginatedResults { + totals: Record; + results: ResolverEvent[]; + // content holder for any other extra aggregation counts + extras?: Record>; +} + interface PaginationCursor { timestamp: number; eventID: string; } -function urlEncodeCursor(data: PaginationCursor) { +function urlEncodeCursor(data: PaginationCursor): string { const value = JSON.stringify(data); return Buffer.from(value, 'utf8') .toString('base64') @@ -56,10 +63,16 @@ export function getPaginationParams(limit: number, after?: string): PaginationPa return { size: limit }; } -export function paginate(pagination: PaginationParams, field: string, query: JsonObject) { +export function paginate( + pagination: PaginationParams, + tiebreaker: string, + aggregator: string, + query: JsonObject +): JsonObject { const { size, timestamp, eventID } = pagination; - query.sort = [{ '@timestamp': 'asc' }, { [field]: 'asc' }]; - query.aggs = { total: { value_count: { field } } }; + query.sort = [{ '@timestamp': 'asc' }, { [tiebreaker]: 'asc' }]; + query.aggs = query.aggs || {}; + query.aggs = Object.assign({}, query.aggs, { totals: { terms: { field: aggregator, size } } }); query.size = size; if (timestamp && eventID) { query.search_after = [timestamp, eventID] as Array; @@ -67,25 +80,28 @@ export function paginate(pagination: PaginationParams, field: string, query: Jso return query; } -export function paginatedResults( - response: SearchResponse -): { total: number; results: ResolverEvent[]; nextCursor: string | null } { - const total = response.aggregations?.total?.value || 0; - if (response.hits.hits.length === 0) { - return { total, results: [], nextCursor: null }; +export function buildPaginationCursor(total: number, results: ResolverEvent[]): string | null { + if (total > results.length && results.length > 0) { + const lastResult = results[results.length - 1]; + const cursor = { + timestamp: lastResult['@timestamp'], + eventID: entityId(lastResult), + }; + return urlEncodeCursor(cursor); } + return null; +} - const results: ResolverEvent[] = []; - for (const hit of response.hits.hits) { - results.push(hit._source); +export function paginatedResults(response: SearchResponse): PaginatedResults { + if (response.hits.hits.length === 0) { + return { totals: {}, results: [] }; } - // results will be at least 1 because of length check at the top of the function - const next = results[results.length - 1]; - const cursor = { - timestamp: next['@timestamp'], - eventID: extractEventID(next), - }; + const totals = response.aggregations?.totals?.buckets?.reduce( + (cummulative: any, bucket: any) => ({ ...cummulative, [bucket.key]: bucket.doc_count }), + {} + ); - return { total, results, nextCursor: urlEncodeCursor(cursor) }; + const results = response.hits.hits.map(hit => hit._source); + return { totals, results }; } diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/tree.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/tree.ts new file mode 100644 index 0000000000000..5a55c23b90873 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/utils/tree.ts @@ -0,0 +1,230 @@ +/* + * 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 _ from 'lodash'; +import { + ResolverEvent, + ResolverNode, + ResolverNodeStats, + ResolverNodePagination, +} from '../../../../common/types'; +import { entityId, parentEntityId } from '../../../../common/models/event'; +import { buildPaginationCursor } from './pagination'; + +type ExtractFunction = (event: ResolverEvent) => string | undefined; + +function createNode(id: string): ResolverNode { + return { id, children: [], pagination: {}, events: [], lifecycle: [] }; +} +/** + * This class aids in constructing a tree of process events. It works in the following way: + * + * 1. We construct a tree structure starting with the root node for the event we're requesting. + * 2. We leverage the ability to pass hashes and arrays by reference to construct a fast cache of + * process identifiers that updates the tree structure as we push values into the cache. + * + * When we query a single level of results for child process events we have a flattened, sorted result + * list that we need to add into a constructed tree. We also need to signal in an API response whether + * or not there are more child processes events that we have not yet retrieved, and, if so, for what parent + * process. So, at the end of our tree construction we have a relational layout of the events with no + * pagination information for the given parent nodes. In order to actually construct both the tree and + * insert the pagination information we basically do the following: + * + * 1. Using a terms aggregation query, we return an approximate roll-up of the number of child process + * "creation" events, this gives us an estimation of the number of associated children per parent + * 2. We feed these child process creation event "unique identifiers" (basically a process.entity_id) + * into a second query to get the current state of the process via its "lifecycle" events. + * 3. We construct the tree above with the "lifecycle" events. + * 4. Using the terms query results, we mark each non-leaf node with the number of expected children, if our + * tree has less children than expected, we create a pagination cursor to indicate "we have a truncated set + * of values". + * 5. We mark each leaf node (the last level of the tree we're constructing) with a "null" for the expected + * number of children to indicate "we have not yet attempted to get any children". + * + * Following this scheme, we use exactly 2 queries per level of children that we return--one for the pagination + * and one for the lifecycle events of the processes. The downside to this is that we need to dynamically expand + * the number of documents we can retrieve per level due to the exponential fanout of child processes, + * what this means is that noisy neighbors for a given level may hide other child process events that occur later + * temporally in the same level--so, while a heavily forking process might get shown, maybe the actually malicious + * event doesn't show up in the tree at the beginning. + */ + +export class Tree { + protected cache: Map; + protected root: ResolverNode; + protected id: string; + + constructor(id: string) { + const root = createNode(id); + this.id = id; + this.cache = new Map(); + this.root = root; + this.cache.set(id, root); + } + + public render(): ResolverNode { + return this.root; + } + + public ids(): string[] { + return [...this.cache.keys()]; + } + + public static async merge( + childrenPromise: Promise, + ancestorsPromise: Promise, + eventsPromise: Promise + ): Promise { + const [children, ancestors, events] = await Promise.all([ + childrenPromise, + ancestorsPromise, + eventsPromise, + ]); + + /* + * we only allow for merging when we have partial trees that + * represent the same root node + */ + const rootID = children.id; + if (rootID !== ancestors.id || rootID !== events.id) { + throw new Error('cannot merge trees with different roots'); + } + + Object.entries(ancestors.cache).forEach(([id, node]) => { + if (rootID !== id) { + children.cache.set(id, node); + } + }); + + children.root.lifecycle = ancestors.root.lifecycle; + children.root.ancestors = ancestors.root.ancestors; + children.root.events = events.root.events; + + Object.assign(children.root.pagination, ancestors.root.pagination, events.root.pagination); + + return children; + } + + public addEvent(...events: ResolverEvent[]): void { + events.forEach(event => { + const id = entityId(event); + + this.ensureCache(id); + const currentNode = this.cache.get(id); + if (currentNode !== undefined) { + currentNode.events.push(event); + } + }); + } + + public addAncestor(id: string, ...events: ResolverEvent[]): void { + events.forEach(event => { + const ancestorID = entityId(event); + if (this.cache.get(ancestorID) === undefined) { + const newParent = createNode(ancestorID); + this.cache.set(ancestorID, newParent); + if (!this.root.ancestors) { + this.root.ancestors = []; + } + this.root.ancestors.push(newParent); + } + const currentAncestor = this.cache.get(ancestorID); + if (currentAncestor !== undefined) { + currentAncestor.lifecycle.push(event); + } + }); + } + + public addStats(id: string, stats: ResolverNodeStats): void { + this.ensureCache(id); + const currentNode = this.cache.get(id); + if (currentNode !== undefined) { + currentNode.stats = stats; + } + } + + public setNextAncestor(next: string | null): void { + this.root.pagination.nextAncestor = next; + } + + public setNextEvent(next: string | null): void { + this.root.pagination.nextEvent = next; + } + + public setNextAlert(next: string | null): void { + this.root.pagination.nextAlert = next; + } + + public addChild(...events: ResolverEvent[]): void { + events.forEach(event => { + const id = entityId(event); + const parentID = parentEntityId(event); + + this.ensureCache(parentID); + let currentNode = this.cache.get(id); + + if (currentNode === undefined) { + currentNode = createNode(id); + this.cache.set(id, currentNode); + if (parentID !== undefined) { + const parentNode = this.cache.get(parentID); + if (parentNode !== undefined) { + parentNode.children.push(currentNode); + } + } + } + currentNode.lifecycle.push(event); + }); + } + + public markLeafNode(...ids: string[]): void { + ids.forEach(id => { + this.ensureCache(id); + const currentNode = this.cache.get(id); + if (currentNode !== undefined && !currentNode.pagination.nextChild) { + currentNode.pagination.nextChild = null; + } + }); + } + + public paginateEvents(totals: Record, events: ResolverEvent[]): void { + return this.paginate(entityId, 'nextEvent', totals, events); + } + + public paginateChildren(totals: Record, children: ResolverEvent[]): void { + return this.paginate(parentEntityId, 'nextChild', totals, children); + } + + private paginate( + grouper: ExtractFunction, + attribute: keyof ResolverNodePagination, + totals: Record, + records: ResolverEvent[] + ): void { + const grouped = _.groupBy(records, grouper); + Object.entries(totals).forEach(([id, total]) => { + if (this.cache.get(id) !== undefined) { + if (grouped[id]) { + /* + * if we have any results, attempt to build a pagination cursor, the function + * below hands back a null value if no cursor is necessary because we have + * all of the records. + */ + const currentNode = this.cache.get(id); + if (currentNode !== undefined) { + currentNode.pagination[attribute] = buildPaginationCursor(total, grouped[id]); + } + } + } + }); + } + + private ensureCache(id: string | undefined): void { + if (id === undefined || this.cache.get(id) === undefined) { + throw new Error('dangling node'); + } + } +} diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index 38364033cb70b..941dedc3d1093 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -274,10 +274,16 @@ PUT _ilm/policy/event_log_policy "hot": { "actions": { "rollover": { - "max_size": "5GB", + "max_size": "50GB", "max_age": "30d" } } + }, + "delete": { + "min_age": "90d", + "actions": { + "delete": {} + } } } } @@ -285,10 +291,11 @@ PUT _ilm/policy/event_log_policy ``` This means that ILM would "rollover" the current index, say -`.kibana-event-log-000001` by creating a new index `.kibana-event-log-000002`, +`.kibana-event-log-8.0.0-000001` by creating a new index `.kibana-event-log-8.0.0-000002`, which would "inherit" everything from the index template, and then ILM will set the write index of the the alias to the new index. This would happen -when the original index grew past 5 GB, or was created more than 30 days ago. +when the original index grew past 50 GB, or was created more than 30 days ago. +After rollover, the indices will be removed after 90 days to avoid disks to fill up. For more relevant information on ILM, see: [getting started with ILM doc][] and [write index alias behavior][]: diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index f487e9262e50e..0a858969c4f6a 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -86,6 +86,10 @@ }, "saved_objects": { "properties": { + "rel": { + "type": "keyword", + "ignore_above": 1024 + }, "namespace": { "type": "keyword", "ignore_above": 1024 diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index 9c923fe77d035..57fe90a8e876e 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -65,6 +65,7 @@ export const EventSchema = schema.maybe( saved_objects: schema.maybe( schema.arrayOf( schema.object({ + rel: ecsString(), namespace: ecsString(), id: ecsString(), type: ecsString(), diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index 8cc2c74b60e57..fd149d132031e 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -24,6 +24,11 @@ exports.EcsKibanaExtensionsMappings = { saved_objects: { type: 'nested', properties: { + // relation; currently only supports "primary" or not set + rel: { + type: 'keyword', + ignore_above: 1024, + }, // relevant kibana space namespace: { type: 'keyword', @@ -58,6 +63,7 @@ exports.EcsEventLogProperties = [ 'user.name', 'kibana.server_uuid', 'kibana.alerting.instance_id', + 'kibana.saved_objects.rel', 'kibana.saved_objects.namespace', 'kibana.saved_objects.id', 'kibana.saved_objects.name', diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index f79962a324131..66c16d0ddf383 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -236,6 +236,13 @@ describe('queryEventsBySavedObject', () => { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', + }, + }, + }, { term: { 'kibana.saved_objects.type': { @@ -319,6 +326,13 @@ describe('queryEventsBySavedObject', () => { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', + }, + }, + }, { term: { 'kibana.saved_objects.type': { @@ -388,6 +402,13 @@ describe('queryEventsBySavedObject', () => { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', + }, + }, + }, { term: { 'kibana.saved_objects.type': { diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index 47d273b9981e3..c0ff87234c09d 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -7,7 +7,7 @@ import { reject, isUndefined } from 'lodash'; import { SearchResponse, Client } from 'elasticsearch'; import { Logger, ClusterClient } from '../../../../../src/core/server'; -import { IEvent } from '../types'; +import { IEvent, SAVED_OBJECT_REL_PRIMARY } from '../types'; import { FindOptionsType } from '../event_log_client'; export type EsClusterClient = Pick; @@ -155,6 +155,13 @@ export class ClusterClientAdapter { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: SAVED_OBJECT_REL_PRIMARY, + }, + }, + }, { term: { 'kibana.saved_objects.type': { diff --git a/x-pack/plugins/event_log/server/es/documents.ts b/x-pack/plugins/event_log/server/es/documents.ts index a6af209d6d3a0..91b3db554964f 100644 --- a/x-pack/plugins/event_log/server/es/documents.ts +++ b/x-pack/plugins/event_log/server/es/documents.ts @@ -31,12 +31,18 @@ export function getIlmPolicy() { hot: { actions: { rollover: { - max_size: '5GB', + max_size: '50GB', max_age: '30d', // max_docs: 1, // you know, for testing }, }, }, + delete: { + min_age: '90d', + actions: { + delete: {}, + }, + }, }, }, }; diff --git a/x-pack/plugins/event_log/server/event_logger.test.ts b/x-pack/plugins/event_log/server/event_logger.test.ts index 6a745931420c0..2bda194a65d13 100644 --- a/x-pack/plugins/event_log/server/event_logger.test.ts +++ b/x-pack/plugins/event_log/server/event_logger.test.ts @@ -150,6 +150,35 @@ describe('EventLogger', () => { message = await waitForLogMessage(systemLogger); expect(message).toMatch(/invalid event logged.*action.*undefined.*/); }); + + test('logs warnings when writing invalid events', async () => { + service.registerProviderActions('provider', ['action-a']); + eventLogger = service.getLogger({}); + + eventLogger.logEvent(({ event: { PROVIDER: 'provider' } } as unknown) as IEvent); + let message = await waitForLogMessage(systemLogger); + expect(message).toMatch(/invalid event logged.*provider.*undefined.*/); + + const event: IEvent = { + event: { + provider: 'provider', + action: 'action-a', + }, + kibana: { + saved_objects: [ + { + rel: 'ZZZ-primary', + namespace: 'default', + type: 'event_log_test', + id: '123', + }, + ], + }, + }; + eventLogger.logEvent(event); + message = await waitForLogMessage(systemLogger); + expect(message).toMatch(/invalid rel property.*ZZZ-primary.*/); + }); }); // return the next logged event; throw if not an event diff --git a/x-pack/plugins/event_log/server/event_logger.ts b/x-pack/plugins/event_log/server/event_logger.ts index bcfd7bd45a6f5..1a710a6fa4865 100644 --- a/x-pack/plugins/event_log/server/event_logger.ts +++ b/x-pack/plugins/event_log/server/event_logger.ts @@ -19,6 +19,7 @@ import { ECS_VERSION, EventSchema, } from './types'; +import { SAVED_OBJECT_REL_PRIMARY } from './types'; type SystemLogger = Plugin['systemLogger']; @@ -118,6 +119,8 @@ const RequiredEventSchema = schema.object({ action: schema.string({ minLength: 1 }), }); +const ValidSavedObjectRels = new Set([undefined, SAVED_OBJECT_REL_PRIMARY]); + function validateEvent(eventLogService: IEventLogService, event: IEvent): IValidatedEvent { if (event?.event == null) { throw new Error(`no "event" property`); @@ -137,7 +140,17 @@ function validateEvent(eventLogService: IEventLogService, event: IEvent): IValid } // could throw an error - return EventSchema.validate(event); + const result = EventSchema.validate(event); + + if (result?.kibana?.saved_objects?.length) { + for (const so of result?.kibana?.saved_objects) { + if (!ValidSavedObjectRels.has(so.rel)) { + throw new Error(`invalid rel property in saved_objects: "${so.rel}"`); + } + } + } + + return result; } export const EVENT_LOGGED_PREFIX = `event logged: `; diff --git a/x-pack/plugins/event_log/server/index.ts b/x-pack/plugins/event_log/server/index.ts index b7fa25cb6eb9c..0612b5319c15b 100644 --- a/x-pack/plugins/event_log/server/index.ts +++ b/x-pack/plugins/event_log/server/index.ts @@ -8,6 +8,12 @@ import { PluginInitializerContext } from 'src/core/server'; import { ConfigSchema } from './types'; import { Plugin } from './plugin'; -export { IEventLogService, IEventLogger, IEventLogClientService, IEvent } from './types'; +export { + IEventLogService, + IEventLogger, + IEventLogClientService, + IEvent, + SAVED_OBJECT_REL_PRIMARY, +} from './types'; export const config = { schema: ConfigSchema }; export const plugin = (context: PluginInitializerContext) => new Plugin(context); diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index baf53ef447914..58be6707b0373 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -13,6 +13,8 @@ import { IEvent } from '../generated/schemas'; import { FindOptionsType } from './event_log_client'; import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter'; +export const SAVED_OBJECT_REL_PRIMARY = 'primary'; + export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), logEntries: schema.boolean({ defaultValue: false }), diff --git a/x-pack/plugins/graph/public/application.ts b/x-pack/plugins/graph/public/application.ts index 35ec0bb2bf6ce..fa479b1b06d5e 100644 --- a/x-pack/plugins/graph/public/application.ts +++ b/x-pack/plugins/graph/public/application.ts @@ -10,7 +10,7 @@ import angular from 'angular'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import '../../../../webpackShims/ace'; -// required for i18nIdDirective +// required for i18nIdDirective and `ngSanitize` angular module import 'angular-sanitize'; // required for ngRoute import 'angular-route'; diff --git a/x-pack/plugins/graph/server/saved_objects/migrations.ts b/x-pack/plugins/graph/server/saved_objects/migrations.ts index e77d2ea0fb7c9..beb31d548c670 100644 --- a/x-pack/plugins/graph/server/saved_objects/migrations.ts +++ b/x-pack/plugins/graph/server/saved_objects/migrations.ts @@ -8,7 +8,7 @@ import { get } from 'lodash'; import { SavedObjectUnsanitizedDoc } from 'kibana/server'; export const graphMigrations = { - '7.0.0': (doc: SavedObjectUnsanitizedDoc) => { + '7.0.0': (doc: SavedObjectUnsanitizedDoc) => { // Set new "references" attribute doc.references = doc.references || []; // Migrate index pattern diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx index d4d53b81109c6..f2ac34ccb752f 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx @@ -13,6 +13,9 @@ import { EuiText, EuiFormRow, EuiButtonEmpty, + EuiCheckbox, + EuiToolTip, + EuiIcon, EuiFieldSearch, } from '@elastic/eui'; import { IFieldType } from 'src/plugins/data/public'; @@ -57,6 +60,7 @@ interface Props { groupBy?: string; filterQuery?: string; sourceId?: string; + alertOnNoData?: boolean; }; alertsContext: AlertsContextValue; setAlertParams(key: string, value: any): void; @@ -282,6 +286,28 @@ export const Expressions: React.FC = props => { + + + {i18n.translate('xpack.infra.metrics.alertFlyout.alertOnNoData', { + defaultMessage: "Alert me if there's no data", + })}{' '} + + + + + } + checked={alertParams.alertOnNoData} + onChange={e => setAlertParams('alertOnNoData', e.target.checked)} + /> + { + const gtText = i18n.translate('xpack.infra.metrics.alerting.threshold.gtComparator', { + defaultMessage: 'greater than', + }); + const ltText = i18n.translate('xpack.infra.metrics.alerting.threshold.ltComparator', { + defaultMessage: 'less than', + }); + const eqText = i18n.translate('xpack.infra.metrics.alerting.threshold.eqComparator', { + defaultMessage: 'equal to', + }); + + switch (comparator) { + case Comparator.BETWEEN: + return i18n.translate('xpack.infra.metrics.alerting.threshold.betweenComparator', { + defaultMessage: 'between', + }); + case Comparator.OUTSIDE_RANGE: + return i18n.translate('xpack.infra.metrics.alerting.threshold.outsideRangeComparator', { + defaultMessage: 'not between', + }); + case Comparator.GT: + return gtText; + case Comparator.LT: + return ltText; + case Comparator.GT_OR_EQ: + case Comparator.LT_OR_EQ: + if (threshold[0] === currentValue) return eqText; + else if (threshold[0] < currentValue) return ltText; + return gtText; + } +}; + +const thresholdToI18n = ([a, b]: number[]) => { + if (typeof b === 'undefined') return a; + return i18n.translate('xpack.infra.metrics.alerting.threshold.thresholdRange', { + defaultMessage: '{a} and {b}', + values: { a, b }, + }); +}; + +export const buildFiredAlertReason: (alertResult: { + metric: string; + comparator: Comparator; + threshold: number[]; + currentValue: number; +}) => string = ({ metric, comparator, threshold, currentValue }) => + i18n.translate('xpack.infra.metrics.alerting.threshold.firedAlertReason', { + defaultMessage: + '{metric} is {comparator} a threshold of {threshold} (current value is {currentValue})', + values: { + metric, + comparator: comparatorToI18n(comparator, threshold, currentValue), + threshold: thresholdToI18n(threshold), + currentValue, + }, + }); + +export const buildNoDataAlertReason: (alertResult: { + metric: string; + timeSize: number; + timeUnit: string; +}) => string = ({ metric, timeSize, timeUnit }) => + i18n.translate('xpack.infra.metrics.alerting.threshold.noDataAlertReason', { + defaultMessage: '{metric} has reported no data over the past {interval}', + values: { + metric, + interval: `${timeSize}${timeUnit}`, + }, + }); + +export const buildErrorAlertReason = (metric: string) => + i18n.translate('xpack.infra.metrics.alerting.threshold.errorAlertReason', { + defaultMessage: 'Elasticsearch failed when attempting to query data for {metric}', + values: { + metric, + }, + }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 24b6ba2ec378b..0007b8bd719f4 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -34,6 +34,8 @@ services.callCluster.mockImplementation(async (_: string, { body, index }: any) } if (metric === 'test.metric.2') { return mocks.alternateMetricResponse; + } else if (metric === 'test.metric.3') { + return mocks.emptyMetricResponse; } return mocks.basicMetricResponse; }); @@ -161,9 +163,9 @@ describe('The metric threshold alert type', () => { await execute(Comparator.GT, [0.75]); const { action } = mostRecentAction(instanceID); expect(action.group).toBe('*'); - expect(action.valueOf.condition0).toBe(1); - expect(action.thresholdOf.condition0).toStrictEqual([0.75]); - expect(action.metricOf.condition0).toBe('test.metric.1'); + expect(action.reason).toContain('current value is 1'); + expect(action.reason).toContain('threshold of 0.75'); + expect(action.reason).toContain('test.metric.1'); }); test('fetches the index pattern dynamically', async () => { await execute(Comparator.LT, [17], 'alternate'); @@ -271,12 +273,14 @@ describe('The metric threshold alert type', () => { const instanceID = 'test-*'; await execute(Comparator.GT_OR_EQ, [1.0], [3.0]); const { action } = mostRecentAction(instanceID); - expect(action.valueOf.condition0).toBe(1); - expect(action.valueOf.condition1).toBe(3.5); - expect(action.thresholdOf.condition0).toStrictEqual([1.0]); - expect(action.thresholdOf.condition1).toStrictEqual([3.0]); - expect(action.metricOf.condition0).toBe('test.metric.1'); - expect(action.metricOf.condition1).toBe('test.metric.2'); + const reasons = action.reason.split('\n'); + expect(reasons.length).toBe(2); + expect(reasons[0]).toContain('test.metric.1'); + expect(reasons[1]).toContain('test.metric.2'); + expect(reasons[0]).toContain('current value is 1'); + expect(reasons[1]).toContain('current value is 3.5'); + expect(reasons[0]).toContain('threshold of 1'); + expect(reasons[1]).toContain('threshold of 3'); }); }); describe('querying with the count aggregator', () => { @@ -305,4 +309,32 @@ describe('The metric threshold alert type', () => { expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); }); + describe("querying a metric that hasn't reported data", () => { + const instanceID = 'test-*'; + const execute = (alertOnNoData: boolean) => + executor({ + services, + params: { + criteria: [ + { + ...baseCriterion, + comparator: Comparator.GT, + threshold: 1, + metric: 'test.metric.3', + }, + ], + alertOnNoData, + }, + }); + test('sends a No Data alert when configured to do so', async () => { + await execute(true); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.NO_DATA); + }); + test('does not send a No Data alert when not configured to do so', async () => { + await execute(false); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.NO_DATA); + }); + }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 946f1c14bf593..bd77e5e2daf42 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -12,6 +12,13 @@ import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler'; import { getAllCompositeData } from '../../../utils/get_all_composite_data'; import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; import { MetricExpressionParams, Comparator, Aggregators, AlertStates } from './types'; +import { + buildErrorAlertReason, + buildFiredAlertReason, + buildNoDataAlertReason, + DOCUMENT_COUNT_I18N, + stateToAlertMessage, +} from './messages'; import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/server'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { getDateHistogramOffset } from '../../snapshot/query_helpers'; @@ -51,17 +58,19 @@ const getCurrentValueFromAggregations = ( const getParsedFilterQuery: ( filterQuery: string | undefined -) => Record = filterQuery => { +) => Record | Array> = filterQuery => { if (!filterQuery) return {}; try { return JSON.parse(filterQuery).bool; } catch (e) { - return { - query_string: { - query: filterQuery, - analyze_wildcard: true, + return [ + { + query_string: { + query: filterQuery, + analyze_wildcard: true, + }, }, - }; + ]; } }; @@ -159,8 +168,12 @@ export const getElasticsearchMetricQuery = ( return { query: { bool: { - filter: [...rangeFilters, ...metricFieldFilters], - ...parsedFilterQuery, + filter: [ + ...rangeFilters, + ...metricFieldFilters, + ...(Array.isArray(parsedFilterQuery) ? parsedFilterQuery : []), + ], + ...(!Array.isArray(parsedFilterQuery) ? parsedFilterQuery : {}), }, }, size: 0, @@ -233,6 +246,7 @@ const getMetric: ( body: searchBody, index, }); + return { '*': getCurrentValueFromAggregations(result.aggregations, aggType) }; } catch (e) { return { '*': undefined }; // Trigger an Error state @@ -251,24 +265,14 @@ const comparatorMap = { [Comparator.LT_OR_EQ]: (a: number, [b]: number[]) => a <= b, }; -const mapToConditionsLookup = ( - list: any[], - mapFn: (value: any, index: number, array: any[]) => unknown -) => - list - .map(mapFn) - .reduce( - (result: Record, value, i) => ({ ...result, [`condition${i}`]: value }), - {} - ); - export const createMetricThresholdExecutor = (alertUUID: string) => async function({ services, params }: AlertExecutorOptions) { - const { criteria, groupBy, filterQuery, sourceId } = params as { + const { criteria, groupBy, filterQuery, sourceId, alertOnNoData } = params as { criteria: MetricExpressionParams[]; groupBy: string | undefined; filterQuery: string | undefined; sourceId?: string; + alertOnNoData: boolean; }; const alertResults = await Promise.all( @@ -279,9 +283,11 @@ export const createMetricThresholdExecutor = (alertUUID: string) => const { threshold, comparator } = criterion; const comparisonFunction = comparatorMap[comparator]; return mapValues(currentValues, value => ({ + ...criterion, + metric: criterion.metric ?? DOCUMENT_COUNT_I18N, + currentValue: value, shouldFire: value !== undefined && value !== null && comparisonFunction(value, threshold), - currentValue: value, isNoData: value === null, isError: value === undefined, })); @@ -299,23 +305,43 @@ export const createMetricThresholdExecutor = (alertUUID: string) => // whole alert is in a No Data/Error state const isNoData = alertResults.some(result => result[group].isNoData); const isError = alertResults.some(result => result[group].isError); - if (shouldAlertFire) { + + const nextState = isError + ? AlertStates.ERROR + : isNoData + ? AlertStates.NO_DATA + : shouldAlertFire + ? AlertStates.ALERT + : AlertStates.OK; + + let reason; + if (nextState === AlertStates.ALERT) { + reason = alertResults.map(result => buildFiredAlertReason(result[group])).join('\n'); + } + if (alertOnNoData) { + if (nextState === AlertStates.NO_DATA) { + reason = alertResults + .filter(result => result[group].isNoData) + .map(result => buildNoDataAlertReason(result[group])) + .join('\n'); + } else if (nextState === AlertStates.ERROR) { + reason = alertResults + .filter(result => result[group].isError) + .map(result => buildErrorAlertReason(result[group].metric)) + .join('\n'); + } + } + if (reason) { alertInstance.scheduleActions(FIRED_ACTIONS.id, { group, - valueOf: mapToConditionsLookup(alertResults, result => result[group].currentValue), - thresholdOf: mapToConditionsLookup(criteria, criterion => criterion.threshold), - metricOf: mapToConditionsLookup(criteria, criterion => criterion.metric), + alertState: stateToAlertMessage[nextState], + reason, }); } + // Future use: ability to fetch display current alert state alertInstance.replaceState({ - alertState: isError - ? AlertStates.ERROR - : isNoData - ? AlertStates.NO_DATA - : shouldAlertFire - ? AlertStates.ALERT - : AlertStates.OK, + alertState: nextState, }); } }; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 3415ae9873bfb..029491c1168cf 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -55,27 +55,18 @@ export async function registerMetricThresholdAlertType( } ); - const valueOfActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.valueOfActionVariableDescription', + const alertStateActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.alertStateActionVariableDescription', { - defaultMessage: - 'Record of the current value of the watched metric; grouped by condition, i.e valueOf.condition0, valueOf.condition1, etc.', - } - ); - - const thresholdOfActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.thresholdOfActionVariableDescription', - { - defaultMessage: - 'Record of the alerting threshold; grouped by condition, i.e thresholdOf.condition0, thresholdOf.condition1, etc.', + defaultMessage: 'Current state of the alert', } ); - const metricOfActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.metricOfActionVariableDescription', + const reasonActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.reasonActionVariableDescription', { defaultMessage: - 'Record of the watched metric; grouped by condition, i.e metricOf.condition0, metricOf.condition1, etc.', + 'A description of why the alert is in this state, including which metrics have crossed which thresholds', } ); @@ -88,6 +79,7 @@ export async function registerMetricThresholdAlertType( groupBy: schema.maybe(schema.string()), filterQuery: schema.maybe(schema.string()), sourceId: schema.string(), + alertOnNoData: schema.maybe(schema.boolean()), }), }, defaultActionGroupId: FIRED_ACTIONS.id, @@ -96,9 +88,8 @@ export async function registerMetricThresholdAlertType( actionVariables: { context: [ { name: 'group', description: groupActionVariableDescription }, - { name: 'valueOf', description: valueOfActionVariableDescription }, - { name: 'thresholdOf', description: thresholdOfActionVariableDescription }, - { name: 'metricOf', description: metricOfActionVariableDescription }, + { name: 'alertState', description: alertStateActionVariableDescription }, + { name: 'reason', description: reasonActionVariableDescription }, ], }, }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index 66e0a363c8983..fa55f80e472de 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -53,6 +53,14 @@ export const alternateMetricResponse = { }, }; +export const emptyMetricResponse = { + aggregations: { + aggregatedIntervals: { + buckets: [], + }, + }, +}; + export const basicCompositeResponse = { aggregations: { groupings: { diff --git a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts index c5067480fb953..9bc1293799d3c 100644 --- a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts @@ -14,6 +14,7 @@ export const DEFAULT_AGENT_CONFIG = { status: AgentConfigStatus.Active, datasources: [], is_default: true, + monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>, }; export const DEFAULT_AGENT_CONFIGS_PACKAGES = [DefaultPackages.system]; diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts index a7d4e36d16f2a..bff799798ff6e 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts @@ -3,11 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Datasource, NewDatasource, DatasourceInput } from '../types'; +import { Datasource, DatasourceInput } from '../types'; import { storedDatasourceToAgentDatasource } from './datasource_to_agent_datasource'; describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { - const mockNewDatasource: NewDatasource = { + const mockDatasource: Datasource = { + id: 'some-uuid', name: 'mock-datasource', description: '', config_id: '', @@ -15,11 +16,6 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { output_id: '', namespace: 'default', inputs: [], - }; - - const mockDatasource: Datasource = { - ...mockNewDatasource, - id: 'some-uuid', revision: 1, }; @@ -107,17 +103,6 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }); }); - it('uses name for id when id is not provided in case of new datasource', () => { - expect(storedDatasourceToAgentDatasource(mockNewDatasource)).toEqual({ - id: 'mock-datasource', - name: 'mock-datasource', - namespace: 'default', - enabled: true, - use_output: 'default', - inputs: [], - }); - }); - it('returns agent datasource config with flattened input and package stream', () => { expect(storedDatasourceToAgentDatasource({ ...mockDatasource, inputs: [mockInput] })).toEqual({ id: 'some-uuid', diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts index 5deb33ccf10f1..620b663451ea3 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts @@ -3,16 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Datasource, NewDatasource, FullAgentConfigDatasource } from '../types'; +import { Datasource, FullAgentConfigDatasource } from '../types'; import { DEFAULT_OUTPUT } from '../constants'; export const storedDatasourceToAgentDatasource = ( - datasource: Datasource | NewDatasource + datasource: Datasource ): FullAgentConfigDatasource => { - const { name, namespace, enabled, package: pkg, inputs } = datasource; + const { id, name, namespace, enabled, package: pkg, inputs } = datasource; const fullDatasource: FullAgentConfigDatasource = { - id: 'id' in datasource ? datasource.id : name, + id: id || name, name, namespace, enabled, diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts index cb290e61b17e5..a977a1a66e059 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts @@ -11,6 +11,7 @@ describe('Ingest Manager - packageToConfig', () => { name: 'mock-package', title: 'Mock package', version: '0.0.0', + latestVersion: '0.0.0', description: 'description', type: 'mock', categories: [], diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts index fcd3955f3a32f..e3ca7635fdb40 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -60,6 +60,11 @@ export interface AgentEvent { export interface AgentEventSOAttributes extends AgentEvent, SavedObjectAttributes {} +type MetadataValue = string | AgentMetadata; + +export interface AgentMetadata { + [x: string]: MetadataValue; +} interface AgentBase { type: AgentType; active: boolean; @@ -72,19 +77,17 @@ interface AgentBase { config_revision?: number | null; config_newest_revision?: number; last_checkin?: string; + user_provided_metadata: AgentMetadata; + local_metadata: AgentMetadata; } export interface Agent extends AgentBase { id: string; current_error_events: AgentEvent[]; - user_provided_metadata: Record; - local_metadata: Record; access_api_key?: string; status?: string; } export interface AgentSOAttributes extends AgentBase, SavedObjectAttributes { - user_provided_metadata: string; - local_metadata: string; current_error_events?: string; } diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts index 2372caee512af..96121251b133e 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts @@ -3,8 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { SavedObjectAttributes } from 'src/core/public'; import { Datasource, DatasourcePackage, @@ -23,9 +21,10 @@ export interface NewAgentConfig { namespace?: string; description?: string; is_default?: boolean; + monitoring_enabled?: Array<'logs' | 'metrics'>; } -export interface AgentConfig extends NewAgentConfig, SavedObjectAttributes { +export interface AgentConfig extends NewAgentConfig { id: string; status: AgentConfigStatus; datasources: string[] | Datasource[]; @@ -60,4 +59,12 @@ export interface FullAgentConfig { }; datasources: FullAgentConfigDatasource[]; revision?: number; + settings?: { + monitoring: { + use_output?: string; + enabled: boolean; + metrics: boolean; + logs: boolean; + }; + }; } diff --git a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts index 885e0a9316d79..ca61a93d9be93 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts @@ -17,22 +17,29 @@ export interface DatasourceConfigRecordEntry { export type DatasourceConfigRecord = Record; -export interface DatasourceInputStream { +export interface NewDatasourceInputStream { id: string; enabled: boolean; dataset: string; processors?: string[]; config?: DatasourceConfigRecord; vars?: DatasourceConfigRecord; +} + +export interface DatasourceInputStream extends NewDatasourceInputStream { agent_stream?: any; } -export interface DatasourceInput { +export interface NewDatasourceInput { type: string; enabled: boolean; processors?: string[]; config?: DatasourceConfigRecord; vars?: DatasourceConfigRecord; + streams: NewDatasourceInputStream[]; +} + +export interface DatasourceInput extends Omit { streams: DatasourceInputStream[]; } @@ -44,10 +51,11 @@ export interface NewDatasource { enabled: boolean; package?: DatasourcePackage; output_id: string; - inputs: DatasourceInput[]; + inputs: NewDatasourceInput[]; } -export type Datasource = NewDatasource & { +export interface Datasource extends Omit { id: string; + inputs: DatasourceInput[]; revision: number; -}; +} diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index fb6bf235d1e26..f8779a879a049 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -97,6 +97,7 @@ export interface RegistryStream { description?: string; enabled?: boolean; vars?: RegistryVarsEntry[]; + template?: string; } export type RequirementVersion = string; @@ -203,6 +204,8 @@ export interface RegistryVarsEntry { // internal until we need them interface PackageAdditions { title: string; + latestVersion: string; + installedVersion?: string; assets: AssetsGroupedByServiceByType; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx new file mode 100644 index 0000000000000..1e7a14e350229 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx @@ -0,0 +1,101 @@ +/* + * 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 { + EuiButtonEmpty, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiLink, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface Props { + onClose: () => void; +} + +export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => { + return ( + + + +

+ +

+
+
+ + +

+ +

+ + + + ), + forumLink: ( + + + + ), + }} + /> +

+ +

+ + + + ), + }} + /> +

+
+
+ + + + + +
+ ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx index 0f3ddee29fa44..5a06a9a879441 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx @@ -3,35 +3,45 @@ * 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 React, { useState } from 'react'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiText } from '@elastic/eui'; +import { EuiText, EuiLink } from '@elastic/eui'; +import { AlphaFlyout } from './alpha_flyout'; const Message = styled(EuiText).attrs(props => ({ color: 'subdued', textAlign: 'center', + size: 's', }))` padding: ${props => props.theme.eui.paddingSizes.m}; `; -export const AlphaMessaging: React.FC<{}> = () => ( - -

- - +export const AlphaMessaging: React.FC<{}> = () => { + const [isAlphaFlyoutOpen, setIsAlphaFlyoutOpen] = useState(false); + + return ( + <> + +

+ + + + {' – '} - - {' – '} - - -

-
-); + />{' '} + setIsAlphaFlyoutOpen(true)}> + View more details. + +

+ + {isAlphaFlyoutOpen && setIsAlphaFlyoutOpen(false)} />} + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx index 4429b9d8e6b82..1c9bd9107515d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx @@ -92,7 +92,6 @@ function useSuggestions(fieldPrefix: string, search: string) { const res = (await data.indexPatterns.getFieldsForWildcard({ pattern: INDEX_NAME, })) as IFieldType[]; - if (!data || !data.autocomplete) { throw new Error('Missing data plugin'); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx index 92146e9ee5679..9863463e68a01 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx @@ -209,7 +209,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {

diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts index 0d19ecd0cb735..e2fc190e158f9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts @@ -5,12 +5,18 @@ */ import { sendRequest, useRequest } from './use_request'; import { datasourceRouteService } from '../../services'; -import { CreateDatasourceRequest, CreateDatasourceResponse } from '../../types'; +import { + CreateDatasourceRequest, + CreateDatasourceResponse, + UpdateDatasourceRequest, + UpdateDatasourceResponse, +} from '../../types'; import { DeleteDatasourcesRequest, DeleteDatasourcesResponse, GetDatasourcesRequest, GetDatasourcesResponse, + GetOneDatasourceResponse, } from '../../../../../common/types/rest_spec'; export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => { @@ -21,6 +27,17 @@ export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => { }); }; +export const sendUpdateDatasource = ( + datasourceId: string, + body: UpdateDatasourceRequest['body'] +) => { + return sendRequest({ + path: datasourceRouteService.getUpdatePath(datasourceId), + method: 'put', + body: JSON.stringify(body), + }); +}; + export const sendDeleteDatasource = (body: DeleteDatasourcesRequest['body']) => { return sendRequest({ path: datasourceRouteService.getDeletePath(), @@ -36,3 +53,10 @@ export function useGetDatasources(query: GetDatasourcesRequest['query']) { query, }); } + +export const sendGetOneDatasource = (datasourceId: string) => { + return sendRequest({ + path: datasourceRouteService.getInfoPath(datasourceId), + method: 'get', + }); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index 10245e73520f7..4a9cfe02b74ac 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -96,12 +96,28 @@ export const DefaultLayout: React.FunctionComponent = ({ section, childre - setIsSettingsFlyoutOpen(true)}> - - + + + + + + + + setIsSettingsFlyoutOpen(true)}> + + + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx index 0d53ca34a1fef..92c44d86e47c6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx @@ -18,6 +18,7 @@ import { EuiText, EuiComboBox, EuiIconTip, + EuiCheckboxGroup, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -30,7 +31,7 @@ interface ValidationResults { const StyledEuiAccordion = styled(EuiAccordion)` .ingest-active-button { - color: ${props => props.theme.eui.euiColorPrimary}}; + color: ${props => props.theme.eui.euiColorPrimary}; } `; @@ -244,6 +245,68 @@ export const AgentConfigForm: React.FunctionComponent = ({ )} + + + + +

+ +

+
+ + + + +
+ + { + acc[key] = true; + return acc; + }, + { logs: false, metrics: false } + )} + onChange={id => { + if (id !== 'logs' && id !== 'metrics') { + return; + } + + const hasLogs = + agentConfig.monitoring_enabled && agentConfig.monitoring_enabled.indexOf(id) >= 0; + + const previousValues = agentConfig.monitoring_enabled || []; + updateAgentConfig({ + monitoring_enabled: hasLogs + ? previousValues.filter(type => type !== id) + : [...previousValues, id], + }); + }} + /> + +
); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx index 39d882f7fdf65..f1e3fea6a0742 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx @@ -39,17 +39,29 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{

- + {from === 'edit' ? ( + + ) : ( + + )}

- {from === 'config' ? ( + {from === 'edit' ? ( + + ) : from === 'config' ? ( - {agentConfig && from === 'config' ? ( + {agentConfig && (from === 'config' || from === 'edit') ? ( { const updatePackageInfo = (updatedPackageInfo: PackageInfo | undefined) => { if (updatedPackageInfo) { setPackageInfo(updatedPackageInfo); + setFormState('VALID'); } else { setFormState('INVALID'); setPackageInfo(undefined); @@ -152,9 +153,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const cancelUrl = from === 'config' ? CONFIG_URL : PACKAGE_URL; // Save datasource - const [formState, setFormState] = useState< - 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED' - >('INVALID'); + const [formState, setFormState] = useState('INVALID'); const saveDatasource = async () => { setFormState('LOADING'); const result = await sendCreateDatasource(datasource); @@ -174,6 +173,23 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const { error } = await saveDatasource(); if (!error) { history.push(`${AGENT_CONFIG_DETAILS_PATH}${agentConfig ? agentConfig.id : configId}`); + notifications.toasts.addSuccess({ + title: i18n.translate('xpack.ingestManager.createDatasource.addedNotificationTitle', { + defaultMessage: `Successfully added '{datasourceName}'`, + values: { + datasourceName: datasource.name, + }, + }), + text: + agentCount && agentConfig + ? i18n.translate('xpack.ingestManager.createDatasource.addedNotificationMessage', { + defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`, + values: { + agentConfigName: agentConfig.name, + }, + }) + : undefined, + }); } else { notifications.toasts.addError(error, { title: 'Error', @@ -229,6 +245,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { packageInfo={packageInfo} datasource={datasource} updateDatasource={updateDatasource} + validationResults={validationResults!} /> ) : null, }, @@ -240,7 +257,6 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { children: agentConfig && packageInfo ? ( ) => void; validationResults: DatasourceValidationResults; submitAttempted: boolean; -}> = ({ - agentConfig, - packageInfo, - datasource, - updateDatasource, - validationResults, - submitAttempted, -}) => { - // Form show/hide states - +}> = ({ packageInfo, datasource, updateDatasource, validationResults, submitAttempted }) => { const hasErrors = validationResults ? validationHasErrors(validationResults) : false; - // Update datasource's package and config info - useEffect(() => { - const dsPackage = datasource.package; - const currentPkgKey = dsPackage ? `${dsPackage.name}-${dsPackage.version}` : ''; - const pkgKey = `${packageInfo.name}-${packageInfo.version}`; - - // If package has changed, create shell datasource with input&stream values based on package info - if (currentPkgKey !== pkgKey) { - // Existing datasources on the agent config using the package name, retrieve highest number appended to datasource name - const dsPackageNamePattern = new RegExp(`${packageInfo.name}-(\\d+)`); - const dsWithMatchingNames = (agentConfig.datasources as Datasource[]) - .filter(ds => Boolean(ds.name.match(dsPackageNamePattern))) - .map(ds => parseInt(ds.name.match(dsPackageNamePattern)![1], 10)) - .sort(); - - updateDatasource({ - name: `${packageInfo.name}-${ - dsWithMatchingNames.length ? dsWithMatchingNames[dsWithMatchingNames.length - 1] + 1 : 1 - }`, - package: { - name: packageInfo.name, - title: packageInfo.title, - version: packageInfo.version, - }, - inputs: packageToConfigDatasourceInputs(packageInfo), - }); - } - - // If agent config has changed, update datasource's config ID and namespace - if (datasource.config_id !== agentConfig.id) { - updateDatasource({ - config_id: agentConfig.id, - namespace: agentConfig.namespace, - }); - } - }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]); - - // Step B, configure inputs (and their streams) + // Configure inputs (and their streams) // Assume packages only export one datasource for now const renderConfigureInputs = () => packageInfo.datasources && diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx index 792389381eaf0..c4d602c2c2081 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx @@ -17,13 +17,16 @@ import { } from '@elastic/eui'; import { AgentConfig, PackageInfo, Datasource, NewDatasource } from '../../../types'; import { packageToConfigDatasourceInputs } from '../../../services'; +import { Loading } from '../../../components'; +import { DatasourceValidationResults } from './services'; export const StepDefineDatasource: React.FunctionComponent<{ agentConfig: AgentConfig; packageInfo: PackageInfo; datasource: NewDatasource; updateDatasource: (fields: Partial) => void; -}> = ({ agentConfig, packageInfo, datasource, updateDatasource }) => { + validationResults: DatasourceValidationResults; +}> = ({ agentConfig, packageInfo, datasource, updateDatasource, validationResults }) => { // Form show/hide states const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState(false); @@ -64,11 +67,13 @@ export const StepDefineDatasource: React.FunctionComponent<{ } }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]); - return ( + return validationResults ? ( <> } + isInvalid={!!validationResults.description} + error={validationResults.description} > ) : null} + ) : ( + ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts index 85cc758fc4c46..10b30a5696d83 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export type CreateDatasourceFrom = 'package' | 'config'; +export type CreateDatasourceFrom = 'package' | 'config' | 'edit'; +export type DatasourceFormState = 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx index 1eee9f6b0c346..a0418c5f256c4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx @@ -19,7 +19,7 @@ import { import { AgentConfig, Datasource } from '../../../../../types'; import { TableRowActions } from '../../../components/table_row_actions'; import { DangerEuiContextMenuItem } from '../../../components/danger_eui_context_menu_item'; -import { useCapabilities } from '../../../../../hooks'; +import { useCapabilities, useLink } from '../../../../../hooks'; import { useAgentConfigLink } from '../../hooks/use_details_uri'; import { DatasourceDeleteProvider } from '../../../components/datasource_delete_provider'; import { useConfigRefresh } from '../../hooks/use_config'; @@ -56,6 +56,7 @@ export const DatasourcesTable: React.FunctionComponent = ({ }) => { const hasWriteCapabilities = useCapabilities().write; const addDatasourceLink = useAgentConfigLink('add-datasource', { configId: config.id }); + const editDatasourceLink = useLink(`/configs/${config.id}/edit-datasource`); const refreshConfig = useConfigRefresh(); // With the datasources provided on input, generate the list of datasources @@ -201,22 +202,21 @@ export const DatasourcesTable: React.FunctionComponent = ({ {}} + // key="datasourceView" + // > + // + // , {}} - key="datasourceView" - > - - , - // FIXME: implement Edit datasource action - {}} + href={`${editDatasourceLink}/${datasource.id}`} key="datasourceEdit" > = ({ /> , // FIXME: implement Copy datasource action - {}} key="datasourceCopy"> - - , + // {}} key="datasourceCopy"> + // + // , {deleteDatasourcePrompt => { return ( @@ -256,7 +256,7 @@ export const DatasourcesTable: React.FunctionComponent = ({ ], }, ], - [config, hasWriteCapabilities, refreshConfig] + [config, editDatasourceLink, hasWriteCapabilities, refreshConfig] ); return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx new file mode 100644 index 0000000000000..d4c39f21a1ea6 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx @@ -0,0 +1,323 @@ +/* + * 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, { useState, useEffect } from 'react'; +import { useRouteMatch, useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButtonEmpty, + EuiButton, + EuiSteps, + EuiBottomBar, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; +import { AgentConfig, PackageInfo, NewDatasource } from '../../../types'; +import { + useLink, + useCore, + useConfig, + sendUpdateDatasource, + sendGetAgentStatus, + sendGetOneAgentConfig, + sendGetOneDatasource, + sendGetPackageInfoByKey, +} from '../../../hooks'; +import { Loading, Error } from '../../../components'; +import { + CreateDatasourcePageLayout, + ConfirmCreateDatasourceModal, +} from '../create_datasource_page/components'; +import { + DatasourceValidationResults, + validateDatasource, + validationHasErrors, +} from '../create_datasource_page/services'; +import { DatasourceFormState, CreateDatasourceFrom } from '../create_datasource_page/types'; +import { StepConfigureDatasource } from '../create_datasource_page/step_configure_datasource'; +import { StepDefineDatasource } from '../create_datasource_page/step_define_datasource'; + +export const EditDatasourcePage: React.FunctionComponent = () => { + const { notifications } = useCore(); + const { + fleet: { enabled: isFleetEnabled }, + } = useConfig(); + const { + params: { configId, datasourceId }, + } = useRouteMatch(); + const history = useHistory(); + + // Agent config, package info, and datasource states + const [isLoadingData, setIsLoadingData] = useState(true); + const [loadingError, setLoadingError] = useState(); + const [agentConfig, setAgentConfig] = useState(); + const [packageInfo, setPackageInfo] = useState(); + const [datasource, setDatasource] = useState({ + name: '', + description: '', + config_id: '', + enabled: true, + output_id: '', + inputs: [], + }); + + // Retrieve agent config, package, and datasource info + useEffect(() => { + const getData = async () => { + setIsLoadingData(true); + setLoadingError(undefined); + try { + const [{ data: agentConfigData }, { data: datasourceData }] = await Promise.all([ + sendGetOneAgentConfig(configId), + sendGetOneDatasource(datasourceId), + ]); + if (agentConfigData?.item) { + setAgentConfig(agentConfigData.item); + } + if (datasourceData?.item) { + const { id, revision, inputs, ...restOfDatasource } = datasourceData.item; + // Remove `agent_stream` from all stream info, we assign this after saving + const newDatasource = { + ...restOfDatasource, + inputs: inputs.map(input => { + const { streams, ...restOfInput } = input; + return { + ...restOfInput, + streams: streams.map(stream => { + const { agent_stream, ...restOfStream } = stream; + return restOfStream; + }), + }; + }), + }; + setDatasource(newDatasource); + if (datasourceData.item.package) { + const { data: packageData } = await sendGetPackageInfoByKey( + `${datasourceData.item.package.name}-${datasourceData.item.package.version}` + ); + if (packageData?.response) { + setPackageInfo(packageData.response); + setValidationResults(validateDatasource(newDatasource, packageData.response)); + setFormState('VALID'); + } + } + } + } catch (e) { + setLoadingError(e); + } + setIsLoadingData(false); + }; + getData(); + }, [configId, datasourceId]); + + // Retrieve agent count + const [agentCount, setAgentCount] = useState(0); + useEffect(() => { + const getAgentCount = async () => { + const { data } = await sendGetAgentStatus({ configId }); + if (data?.results.total) { + setAgentCount(data.results.total); + } + }; + + if (isFleetEnabled) { + getAgentCount(); + } + }, [configId, isFleetEnabled]); + + // Datasource validation state + const [validationResults, setValidationResults] = useState(); + const hasErrors = validationResults ? validationHasErrors(validationResults) : false; + + // Update datasource method + const updateDatasource = (updatedFields: Partial) => { + const newDatasource = { + ...datasource, + ...updatedFields, + }; + setDatasource(newDatasource); + + // eslint-disable-next-line no-console + console.debug('Datasource updated', newDatasource); + const newValidationResults = updateDatasourceValidation(newDatasource); + const hasValidationErrors = newValidationResults + ? validationHasErrors(newValidationResults) + : false; + if (!hasValidationErrors) { + setFormState('VALID'); + } + }; + + const updateDatasourceValidation = (newDatasource?: NewDatasource) => { + if (packageInfo) { + const newValidationResult = validateDatasource(newDatasource || datasource, packageInfo); + setValidationResults(newValidationResult); + // eslint-disable-next-line no-console + console.debug('Datasource validation results', newValidationResult); + + return newValidationResult; + } + }; + + // Cancel url + const CONFIG_URL = useLink(`${AGENT_CONFIG_DETAILS_PATH}${configId}`); + const cancelUrl = CONFIG_URL; + + // Save datasource + const [formState, setFormState] = useState('INVALID'); + const saveDatasource = async () => { + setFormState('LOADING'); + const result = await sendUpdateDatasource(datasourceId, datasource); + setFormState('SUBMITTED'); + return result; + }; + + const onSubmit = async () => { + if (formState === 'VALID' && hasErrors) { + setFormState('INVALID'); + return; + } + if (agentCount !== 0 && formState !== 'CONFIRM') { + setFormState('CONFIRM'); + return; + } + const { error } = await saveDatasource(); + if (!error) { + history.push(`${AGENT_CONFIG_DETAILS_PATH}${configId}`); + notifications.toasts.addSuccess({ + title: i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationTitle', { + defaultMessage: `Successfully updated '{datasourceName}'`, + values: { + datasourceName: datasource.name, + }, + }), + text: + agentCount && agentConfig + ? i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationMessage', { + defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`, + values: { + agentConfigName: agentConfig.name, + }, + }) + : undefined, + }); + } else { + notifications.toasts.addError(error, { + title: 'Error', + }); + setFormState('VALID'); + } + }; + + const layoutProps = { + from: 'edit' as CreateDatasourceFrom, + cancelUrl, + agentConfig, + packageInfo, + }; + + return ( + + {isLoadingData ? ( + + ) : loadingError || !agentConfig || !packageInfo ? ( + + } + error={ + loadingError || + i18n.translate('xpack.ingestManager.editDatasource.errorLoadingDataMessage', { + defaultMessage: 'There was an error loading this data source information', + }) + } + /> + ) : ( + <> + {formState === 'CONFIRM' && ( + setFormState('VALID')} + /> + )} + + ), + }, + { + title: i18n.translate( + 'xpack.ingestManager.editDatasource.stepConfgiureDatasourceTitle', + { + defaultMessage: 'Select the data you want to collect', + } + ), + children: ( + + ), + }, + ]} + /> + + + + + + + + + + + + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx index 71ada155373bf..ef88aa5d17f1e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx @@ -8,10 +8,14 @@ import { HashRouter as Router, Switch, Route } from 'react-router-dom'; import { AgentConfigListPage } from './list_page'; import { AgentConfigDetailsPage } from './details_page'; import { CreateDatasourcePage } from './create_datasource_page'; +import { EditDatasourcePage } from './edit_datasource_page'; export const AgentConfigApp: React.FunctionComponent = () => ( + + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx index 1fe116ef36090..9f582e7e2fbe6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx @@ -34,6 +34,7 @@ export const CreateAgentConfigFlyout: React.FunctionComponent = ({ onClos description: '', namespace: '', is_default: undefined, + monitoring_enabled: ['logs', 'metrics'], }); const [isLoading, setIsLoading] = useState(false); const [withSysMonitoring, setWithSysMonitoring] = useState(true); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index 1ea162252c741..3dcc19bc4a5ae 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -36,13 +36,11 @@ import { useConfig, useUrlParams, } from '../../../hooks'; -import { AgentConfigDeleteProvider } from '../components'; import { CreateAgentConfigFlyout } from './components'; import { SearchBar } from '../../../components/search_bar'; import { LinkedAgentCount } from '../components'; import { useAgentConfigLink } from '../details_page/hooks/use_details_uri'; import { TableRowActions } from '../components/table_row_actions'; -import { DangerEuiContextMenuItem } from '../components/danger_eui_context_menu_item'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ overflow: 'hidden', @@ -108,30 +106,12 @@ const ConfigRowActions = memo<{ config: AgentConfig; onDelete: () => void }>( defaultMessage="Create data source" /> , - - - - , - - - {deleteAgentConfigsPrompt => { - return ( - deleteAgentConfigsPrompt([config.id], onDelete)} - > - - - ); - }} - , + // + // + // , ]} /> ); @@ -156,7 +136,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { : urlParams.kuery ?? '' ); const { pagination, pageSizeOptions, setPagination } = usePagination(); - const [selectedAgentConfigs, setSelectedAgentConfigs] = useState([]); const history = useHistory(); const isCreateAgentConfigFlyoutOpen = 'create' in urlParams; const setIsCreateAgentConfigFlyoutOpen = useCallback( @@ -321,34 +300,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { /> ) : null} - {selectedAgentConfigs.length ? ( - - - {deleteAgentConfigsPrompt => ( - { - deleteAgentConfigsPrompt( - selectedAgentConfigs.map(agentConfig => agentConfig.id), - () => { - sendRequest(); - setSelectedAgentConfigs([]); - } - ); - }} - > - - - )} - - - ) : null} = () => { items={agentConfigData ? agentConfigData.items : []} itemId="id" columns={columns} - isSelectable={true} - selection={{ - selectable: (agentConfig: AgentConfig) => !agentConfig.is_default, - onSelectionChange: (newSelectedAgentConfigs: AgentConfig[]) => { - setSelectedAgentConfigs(newSelectedAgentConfigs); - }, - }} + isSelectable={false} pagination={{ pageIndex: pagination.currentPage - 1, pageSize: pagination.pageSize, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx new file mode 100644 index 0000000000000..64223efefaab8 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx @@ -0,0 +1,15 @@ +/* + * 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 { EuiIcon } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +export const StyledAlert = styled(EuiIcon)` + color: ${props => props.theme.eui.euiColorWarning}; + padding: 0 5px; +`; + +export const UpdateIcon = () => ; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx index 8ad081cbbabe4..ab7e87b3ad06c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx @@ -30,9 +30,15 @@ export function PackageCard({ showInstalledBadge, status, icons, + ...restProps }: PackageCardProps) { const { toDetailView } = useLinks(); - const url = toDetailView({ name, version }); + let urlVersion = version; + // if this is an installed package, link to the version installed + if ('savedObject' in restProps) { + urlVersion = restProps.savedObject.attributes.version || version; + } + const url = toDetailView({ name, version: urlVersion }); return ( ; +type InstallPackageProps = Pick & { + fromUpdate?: boolean; +}; +type SetPackageInstallStatusProps = Pick & PackageInstallItem; function usePackageInstall({ notifications }: { notifications: NotificationsStart }) { + const { toDetailView } = useLinks(); const [packages, setPackage] = useState({}); const setPackageInstallStatus = useCallback( - ({ name, status }: { name: PackageInfo['name']; status: InstallStatus }) => { + ({ name, status, version }: SetPackageInstallStatusProps) => { + const packageProps: PackageInstallItem = { + status, + version, + }; setPackage((prev: PackagesInstall) => ({ ...prev, - [name]: { status }, + [name]: packageProps, })); }, [] ); + const getPackageInstallStatus = useCallback( + (pkg: string): PackageInstallItem => { + return packages[pkg]; + }, + [packages] + ); + const installPackage = useCallback( - async ({ name, version, title }: InstallPackageProps) => { - setPackageInstallStatus({ name, status: InstallStatus.installing }); + async ({ name, version, title, fromUpdate = false }: InstallPackageProps) => { + const currStatus = getPackageInstallStatus(name); + const newStatus = { ...currStatus, name, status: InstallStatus.installing }; + setPackageInstallStatus(newStatus); const pkgkey = `${name}-${version}`; const res = await sendInstallPackage(pkgkey); if (res.error) { - setPackageInstallStatus({ name, status: InstallStatus.notInstalled }); + if (fromUpdate) { + // if there is an error during update, set it back to the previous version + // as handling of bad update is not implemented yet + setPackageInstallStatus({ ...currStatus, name }); + } else { + setPackageInstallStatus({ name, status: InstallStatus.notInstalled, version }); + } notifications.toasts.addWarning({ title: toMountPoint( { - return packages[pkg].status; - }, - [packages] + [getPackageInstallStatus, notifications.toasts, setPackageInstallStatus, toDetailView] ); const uninstallPackage = useCallback( async ({ name, version, title }: Pick) => { - setPackageInstallStatus({ name, status: InstallStatus.uninstalling }); + setPackageInstallStatus({ name, status: InstallStatus.uninstalling, version }); const pkgkey = `${name}-${version}`; const res = await sendRemovePackage(pkgkey); if (res.error) { - setPackageInstallStatus({ name, status: InstallStatus.installed }); + setPackageInstallStatus({ name, status: InstallStatus.installed, version }); notifications.toasts.addWarning({ title: toMountPoint( ; export function ContentPanel(props: ContentPanelProps) { - const { panel, name, version, assets, title, removable } = props; + const { panel, name, version, assets, title, removable, latestVersion } = props; switch (panel) { case 'settings': return ( @@ -60,6 +60,7 @@ export function ContentPanel(props: ContentPanelProps) { assets={assets} title={title} removable={removable} + latestVersion={latestVersion} /> ); case 'data-sources': diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx index fa3245aec02c5..c82b7ed2297a7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx @@ -20,7 +20,7 @@ export const DataSourcesPanel = ({ name, version }: DataSourcesPanelProps) => { const packageInstallStatus = getPackageInstallStatus(name); // if they arrive at this page and the package is not installed, send them to overview // this happens if they arrive with a direct url or they uninstall while on this tab - if (packageInstallStatus !== InstallStatus.installed) + if (packageInstallStatus.status !== InstallStatus.installed) return ( props.theme.eui.euiSizeM}; `; -const StyledVersion = styled(Version)` - font-size: ${props => props.theme.eui.euiFontSizeS}; - color: ${props => props.theme.eui.euiColorDarkShade}; -`; - type HeaderProps = PackageInfo & { iconType?: IconType }; export function Header(props: HeaderProps) { - const { iconType, name, title, version } = props; + const { iconType, name, title, version, installedVersion, latestVersion } = props; const hasWriteCapabilites = useCapabilities().write; const { toListView } = useLinks(); const ADD_DATASOURCE_URI = useLink(`${EPM_PATH}/${name}-${version}/add-datasource`); - + const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; return ( @@ -59,7 +54,11 @@ export function Header(props: HeaderProps) {

{title} - + + + {version} {updateAvailable && } + +

diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx index 3239d7b90e3c3..1f3eb2cc9362e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx @@ -32,11 +32,12 @@ export function Detail() { const packageInfo = response.data?.response; const title = packageInfo?.title; const name = packageInfo?.name; + const installedVersion = packageInfo?.installedVersion; const status: InstallStatus = packageInfo?.status as any; // track install status state if (name) { - setPackageInstallStatus({ name, status }); + setPackageInstallStatus({ name, status, version: installedVersion || null }); } if (packageInfo) { setInfo({ ...packageInfo, title: title || '' }); @@ -64,7 +65,6 @@ type LayoutProps = PackageInfo & Pick & Pick diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx index cbbf1ce53c4ea..cdad67fd87548 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx @@ -13,19 +13,21 @@ import { ConfirmPackageUninstall } from './confirm_package_uninstall'; import { ConfirmPackageInstall } from './confirm_package_install'; type InstallationButtonProps = Pick & { - disabled: boolean; + disabled?: boolean; + isUpdate?: boolean; }; export function InstallationButton(props: InstallationButtonProps) { - const { assets, name, title, version, disabled = true } = props; + const { assets, name, title, version, disabled = true, isUpdate = false } = props; const hasWriteCapabilites = useCapabilities().write; const installPackage = useInstallPackage(); const uninstallPackage = useUninstallPackage(); const getPackageInstallStatus = useGetPackageInstallStatus(); - const installationStatus = getPackageInstallStatus(name); + const { status: installationStatus } = getPackageInstallStatus(name); const isInstalling = installationStatus === InstallStatus.installing; const isRemoving = installationStatus === InstallStatus.uninstalling; const isInstalled = installationStatus === InstallStatus.installed; + const showUninstallButton = isInstalled || isRemoving; const [isModalVisible, setModalVisible] = useState(false); const toggleModal = useCallback(() => { setModalVisible(!isModalVisible); @@ -36,6 +38,10 @@ export function InstallationButton(props: InstallationButtonProps) { toggleModal(); }, [installPackage, name, title, toggleModal, version]); + const handleClickUpdate = useCallback(() => { + installPackage({ name, version, title, fromUpdate: true }); + }, [installPackage, name, title, version]); + const handleClickUninstall = useCallback(() => { uninstallPackage({ name, version, title }); toggleModal(); @@ -78,6 +84,15 @@ export function InstallationButton(props: InstallationButtonProps) { ); + const updateButton = ( + + + + ); + const uninstallButton = ( - {isInstalled || isRemoving ? uninstallButton : installButton} + {isUpdate ? updateButton : showUninstallButton ? uninstallButton : installButton} {isModalVisible && (isInstalled ? uninstallModal : installModal)}
) : null; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx index 3589a1a9444e1..4d4dba2a64e5a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx @@ -8,11 +8,22 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui'; +import styled from 'styled-components'; import { InstallStatus, PackageInfo } from '../../../../types'; import { useGetDatasources } from '../../../../hooks'; import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../constants'; import { useGetPackageInstallStatus } from '../../hooks'; import { InstallationButton } from './installation_button'; +import { UpdateIcon } from '../../components/icons'; + +const SettingsTitleCell = styled.td` + padding-right: ${props => props.theme.eui.spacerSizes.xl}; + padding-bottom: ${props => props.theme.eui.spacerSizes.m}; +`; + +const UpdatesAvailableMsgContainer = styled.span` + padding-left: ${props => props.theme.eui.spacerSizes.s}; +`; const NoteLabel = () => ( ( defaultMessage="Note:" /> ); +const UpdatesAvailableMsg = () => ( + + + + +); + export const SettingsPanel = ( - props: Pick + props: Pick ) => { + const { name, title, removable, latestVersion, version } = props; const getPackageInstallStatus = useGetPackageInstallStatus(); const { data: datasourcesData } = useGetDatasources({ perPage: 0, page: 1, kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name:${props.name}`, }); - const { name, title, removable } = props; - const packageInstallStatus = getPackageInstallStatus(name); + const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name); const packageHasDatasources = !!datasourcesData?.total; + const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; + const isViewingOldPackage = version < latestVersion; + // hide install/remove options if the user has version of the package is installed + // and this package is out of date or if they do have a version installed but it's not this one + const hideInstallOptions = + (installationStatus === InstallStatus.notInstalled && isViewingOldPackage) || + (installationStatus === InstallStatus.installed && installedVersion !== version); + + const isUpdating = installationStatus === InstallStatus.installing && installedVersion; return ( @@ -43,14 +73,13 @@ export const SettingsPanel = ( - {packageInstallStatus === InstallStatus.notInstalled || - packageInstallStatus === InstallStatus.installing ? ( + {installedVersion !== null && (

-

- -

+ + + + + + + + + + + + + + + +
+ + {installedVersion} + + {updateAvailable && } +
+ + {latestVersion} + +
+ {updateAvailable && ( +

+ +

+ )}

- ) : ( + )} + {!hideInstallOptions && !isUpdating && (
- -

+ + {installationStatus === InstallStatus.notInstalled || + installationStatus === InstallStatus.installing ? ( +
+ +

+ +

+
+ +

+ +

+
+ ) : ( +
+ +

+ +

+
+ +

+ +

+
+ )} + + +

+ +

+
+
+ {packageHasDatasources && removable === true && ( +

+ + + ), }} /> -

-
- -

- -

+

+ )} + {removable === false && ( +

+ + + + ), + }} + /> +

+ )}
)} - - -

- -

-
-
- {packageHasDatasources && removable === true && ( -

- - - - ), - }} - /> -

- )} - {removable === false && ( -

- - - - ), - }} - /> -

- )}
); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx index 05729ccfc1af4..ab168ef1530bd 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx @@ -37,7 +37,7 @@ export function SideNavLinks({ name, version, active }: NavLinkProps) { : p.theme.eui.euiFontWeightRegular}; `; // don't display Data Sources tab if the package is not installed - if (packageInstallStatus !== InstallStatus.installed && panel === 'data-sources') + if (packageInstallStatus.status !== InstallStatus.installed && panel === 'data-sources') return null; return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/helper.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/helper.ts new file mode 100644 index 0000000000000..508190fef0fc2 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/helper.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 { AgentMetadata } from '../../../../types'; + +export function flattenMetadata(metadata: AgentMetadata) { + return Object.entries(metadata).reduce((acc, [key, value]) => { + if (typeof value === 'string') { + acc[key] = value; + + return acc; + } + + Object.entries(flattenMetadata(value)).forEach(([flattenedKey, flattenedValue]) => { + acc[`${key}.${flattenedKey}`] = flattenedValue; + }); + + return acc; + }, {} as { [k: string]: string }); +} +export function unflattenMetadata(flattened: { [k: string]: string }) { + const metadata: AgentMetadata = {}; + + Object.entries(flattened).forEach(([flattenedKey, flattenedValue]) => { + const keyParts = flattenedKey.split('.'); + const lastKey = keyParts.pop(); + + if (!lastKey) { + throw new Error('Invalid metadata'); + } + + let metadataPart = metadata; + keyParts.forEach(keyPart => { + if (!metadataPart[keyPart]) { + metadataPart[keyPart] = {}; + } + + metadataPart = metadataPart[keyPart] as AgentMetadata; + }); + metadataPart[lastKey] = flattenedValue; + }); + + return metadata; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx index ee43385e601c2..aa6da36f8fb6c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx @@ -17,11 +17,13 @@ import { } from '@elastic/eui'; import { MetadataForm } from './metadata_form'; import { Agent } from '../../../../types'; +import { flattenMetadata } from './helper'; interface Props { agent: Agent; flyout: { hide: () => void }; } + export const AgentMetadataFlyout: React.FunctionComponent = ({ agent, flyout }) => { const mapMetadata = (obj: { [key: string]: string } | undefined) => { return Object.keys(obj || {}).map(key => ({ @@ -30,8 +32,8 @@ export const AgentMetadataFlyout: React.FunctionComponent = ({ agent, fly })); }; - const localItems = mapMetadata(agent.local_metadata); - const userProvidedItems = mapMetadata(agent.user_provided_metadata); + const localItems = mapMetadata(flattenMetadata(agent.local_metadata)); + const userProvidedItems = mapMetadata(flattenMetadata(agent.user_provided_metadata)); return ( flyout.hide()} size="s" aria-labelledby="flyoutTitle"> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx index ce28bbdc590b0..af7e8c674db4c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx @@ -22,6 +22,7 @@ import { useAgentRefresh } from '../hooks'; import { useInput, sendRequest } from '../../../../hooks'; import { Agent } from '../../../../types'; import { agentRouteService } from '../../../../services'; +import { flattenMetadata, unflattenMetadata } from './helper'; function useAddMetadataForm(agent: Agent, done: () => void) { const refreshAgent = useAgentRefresh(); @@ -66,15 +67,17 @@ function useAddMetadataForm(agent: Agent, done: () => void) { isLoading: true, }); + const metadata = unflattenMetadata({ + ...flattenMetadata(agent.user_provided_metadata), + [keyInput.value]: valueInput.value, + }); + try { const { error } = await sendRequest({ path: agentRouteService.getUpdatePath(agent.id), method: 'put', body: JSON.stringify({ - user_provided_metadata: { - ...agent.user_provided_metadata, - [keyInput.value]: valueInput.value, - }, + user_provided_metadata: metadata, }), }); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 23fe18b82468c..05264c157434e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -238,7 +238,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const columns = [ { - field: 'local_metadata.host', + field: 'local_metadata.host.hostname', name: i18n.translate('xpack.ingestManager.agentList.hostColumnTitle', { defaultMessage: 'Host', }), diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx index 05d150fd9ae23..70d8e7d6882f8 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx @@ -8,6 +8,7 @@ import styled from 'styled-components'; import { EuiButton, EuiButtonEmpty, + EuiBetaBadge, EuiPanel, EuiText, EuiTitle, @@ -19,10 +20,11 @@ import { EuiFlexItem, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { WithHeaderLayout } from '../../layouts'; import { useLink, useGetAgentConfigs } from '../../hooks'; import { AgentEnrollmentFlyout } from '../fleet/agent_list_page/components'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../../constants'; +import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../../constants'; const OverviewPanel = styled(EuiPanel).attrs(props => ({ paddingSize: 'm', @@ -57,6 +59,11 @@ const OverviewStats = styled(EuiDescriptionList).attrs(props => ({ } `; +const AlphaBadge = styled(EuiBetaBadge)` + vertical-align: top; + margin-left: ${props => props.theme.eui.paddingSizes.s}; +`; + export const IngestManagerOverview: React.FunctionComponent = () => { // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); @@ -79,6 +86,19 @@ export const IngestManagerOverview: React.FunctionComponent = () => { id="xpack.ingestManager.overviewPageTitle" defaultMessage="Ingest Manager" /> +
@@ -213,7 +233,7 @@ export const IngestManagerOverview: React.FunctionComponent = () => { /> - + + (agentConfig: GetAgentConfigsResponseItem) => listAgents(soClient, { showInactive: true, perPage: 0, diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts index 56d6053a1451b..8f07e3ed1de02 100644 --- a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts @@ -7,7 +7,7 @@ import { TypeOf } from '@kbn/config-schema'; import Boom from 'boom'; import { RequestHandler } from 'src/core/server'; import { appContextService, datasourceService } from '../../services'; -import { ensureInstalledPackage } from '../../services/epm/packages'; +import { ensureInstalledPackage, getPackageInfo } from '../../services/epm/packages'; import { GetDatasourcesRequestSchema, GetOneDatasourceRequestSchema, @@ -85,12 +85,13 @@ export const createDatasourceHandler: RequestHandler< pkgName: request.body.package.name, callCluster, }); - + const pkgInfo = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: request.body.package.name, + pkgVersion: request.body.package.version, + }); newData.inputs = (await datasourceService.assignPackageStream( - { - pkgName: request.body.package.name, - pkgVersion: request.body.package.version, - }, + pkgInfo, request.body.inputs )) as TypeOf['inputs']; } @@ -127,13 +128,14 @@ export const updateDatasourceHandler: RequestHandler< const pkg = newData.package || datasource.package; const inputs = newData.inputs || datasource.inputs; if (pkg && (newData.inputs || newData.package)) { - newData.inputs = (await datasourceService.assignPackageStream( - { - pkgName: pkg.name, - pkgVersion: pkg.version, - }, - inputs - )) as TypeOf['inputs']; + const pkgInfo = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: pkg.name, + pkgVersion: pkg.version, + }); + newData.inputs = (await datasourceService.assignPackageStream(pkgInfo, inputs)) as TypeOf< + typeof CreateDatasourceRequestSchema.body + >['inputs']; } const updatedDatasource = await datasourceService.update( diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts index edc9a0a268161..5ee7ee7733220 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts @@ -5,13 +5,14 @@ */ import { IRouter } from 'src/core/server'; import { PLUGIN_ID, FLEET_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants'; +import { IngestManagerConfigType } from '../../../common'; import { getFleetSetupHandler, createFleetSetupHandler, ingestManagerSetupHandler, } from './handlers'; -export const registerRoutes = (router: IRouter) => { +export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) => { // Ingest manager setup router.post( { @@ -23,6 +24,11 @@ export const registerRoutes = (router: IRouter) => { }, ingestManagerSetupHandler ); + + if (!config.fleet.enabled) { + return; + } + // Get Fleet setup router.get( { diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts index 882258e859555..89d8b9e173ffe 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts @@ -56,8 +56,8 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { enrolled_at: { type: 'date' }, access_api_key_id: { type: 'keyword' }, version: { type: 'keyword' }, - user_provided_metadata: { type: 'text' }, - local_metadata: { type: 'text' }, + user_provided_metadata: { type: 'flattened' }, + local_metadata: { type: 'flattened' }, config_id: { type: 'keyword' }, last_updated: { type: 'date' }, last_checkin: { type: 'date' }, @@ -128,6 +128,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { updated_on: { type: 'keyword' }, updated_by: { type: 'keyword' }, revision: { type: 'integer' }, + monitoring_enabled: { type: 'keyword' }, }, }, }, diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts new file mode 100644 index 0000000000000..17758f6e3d7f1 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts @@ -0,0 +1,134 @@ +/* + * 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 { savedObjectsClientMock } from 'src/core/server/mocks'; +import { agentConfigService } from './agent_config'; +import { Output } from '../types'; + +function getSavedObjectMock(configAttributes: any) { + const mock = savedObjectsClientMock.create(); + + mock.get.mockImplementation(async (type: string, id: string) => { + return { + type, + id, + references: [], + attributes: configAttributes, + }; + }); + + return mock; +} + +jest.mock('./output', () => { + return { + outputService: { + getDefaultOutputId: () => 'test-id', + get: (): Output => { + return { + id: 'test-id', + is_default: true, + name: 'default', + // @ts-ignore + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + }; + }, + }, + }; +}); + +describe('agent config', () => { + describe('getFullConfig', () => { + it('should return a config without monitoring if not monitoring is not enabled', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + }); + const config = await agentConfigService.getFullConfig(soClient, 'config'); + + expect(config).toMatchObject({ + id: 'config', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + ca_sha256: undefined, + api_key: undefined, + }, + }, + datasources: [], + revision: 1, + settings: { + monitoring: { + enabled: false, + logs: false, + metrics: false, + }, + }, + }); + }); + + it('should return a config with monitoring if monitoring is enabled for logs', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + monitoring_enabled: ['logs'], + }); + const config = await agentConfigService.getFullConfig(soClient, 'config'); + + expect(config).toMatchObject({ + id: 'config', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + ca_sha256: undefined, + api_key: undefined, + }, + }, + datasources: [], + revision: 1, + settings: { + monitoring: { + use_output: 'default', + enabled: true, + logs: true, + metrics: false, + }, + }, + }); + }); + + it('should return a config with monitoring if monitoring is enabled for metrics', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + monitoring_enabled: ['metrics'], + }); + const config = await agentConfigService.getFullConfig(soClient, 'config'); + + expect(config).toMatchObject({ + id: 'config', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + ca_sha256: undefined, + api_key: undefined, + }, + }, + datasources: [], + revision: 1, + settings: { + monitoring: { + use_output: 'default', + enabled: true, + logs: false, + metrics: true, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index 75bbfc21293c2..7ab6ef1920c18 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -301,28 +301,49 @@ class AgentConfigService { if (!config) { return null; } + const defaultOutput = await outputService.get( + soClient, + await outputService.getDefaultOutputId(soClient) + ); const agentConfig: FullAgentConfig = { id: config.id, outputs: { // TEMPORARY as we only support a default output - ...[ - await outputService.get(soClient, await outputService.getDefaultOutputId(soClient)), - ].reduce((outputs, { config: outputConfig, name, type, hosts, ca_sha256, api_key }) => { - outputs[name] = { - type, - hosts, - ca_sha256, - api_key, - ...outputConfig, - }; - return outputs; - }, {} as FullAgentConfig['outputs']), + ...[defaultOutput].reduce( + (outputs, { config: outputConfig, name, type, hosts, ca_sha256, api_key }) => { + outputs[name] = { + type, + hosts, + ca_sha256, + api_key, + ...outputConfig, + }; + return outputs; + }, + {} as FullAgentConfig['outputs'] + ), }, datasources: (config.datasources as Datasource[]) .filter(datasource => datasource.enabled) .map(ds => storedDatasourceToAgentDatasource(ds)), revision: config.revision, + ...(config.monitoring_enabled && config.monitoring_enabled.length > 0 + ? { + settings: { + monitoring: { + use_output: defaultOutput.name, + enabled: true, + logs: config.monitoring_enabled.indexOf('logs') >= 0, + metrics: config.monitoring_enabled.indexOf('metrics') >= 0, + }, + }, + } + : { + settings: { + monitoring: { enabled: false, logs: false, metrics: false }, + }, + }), }; return agentConfig; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts index c96a81ed9b758..9b1565e7d74aa 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts @@ -11,6 +11,7 @@ import { AgentAction, AgentSOAttributes, AgentEventSOAttributes, + AgentMetadata, } from '../../types'; import { agentConfigService } from '../agent_config'; @@ -28,7 +29,7 @@ export async function agentCheckin( const updateData: { last_checkin: string; default_api_key?: string; - local_metadata?: string; + local_metadata?: AgentMetadata; current_error_events?: string; } = { last_checkin: new Date().toISOString(), diff --git a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts index 175b92b75aca0..43fd5a3ce0ac9 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts @@ -103,7 +103,7 @@ export async function updateAgent( } ) { await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - user_provided_metadata: JSON.stringify(data.userProvidedMetatada), + user_provided_metadata: data.userProvidedMetatada, }); } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts index a34d2e03e9b3d..81afa70ecb818 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts @@ -32,8 +32,8 @@ export async function enroll( config_id: configId, type, enrolled_at: enrolledAt, - user_provided_metadata: JSON.stringify(metadata?.userProvided ?? {}), - local_metadata: JSON.stringify(metadata?.local ?? {}), + user_provided_metadata: metadata?.userProvided ?? {}, + local_metadata: metadata?.local ?? {}, current_error_events: undefined, access_api_key_id: undefined, last_checkin: undefined, diff --git a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts index b182662e0fb4e..11beba1cd7e43 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts @@ -19,8 +19,8 @@ export function savedObjectToAgent(so: SavedObject): Agent { current_error_events: so.attributes.current_error_events ? JSON.parse(so.attributes.current_error_events) : [], - local_metadata: JSON.parse(so.attributes.local_metadata), - user_provided_metadata: JSON.parse(so.attributes.user_provided_metadata), + local_metadata: so.attributes.local_metadata, + user_provided_metadata: so.attributes.user_provided_metadata, access_api_key: undefined, status: undefined, }; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts index b6de083cbe0cb..8140b1e6de470 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts @@ -18,8 +18,8 @@ describe('Agent status service', () => { type: AGENT_TYPE_PERMANENT, attributes: { active: false, - local_metadata: '{}', - user_provided_metadata: '{}', + local_metadata: {}, + user_provided_metadata: {}, }, } as SavedObject); const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); @@ -33,8 +33,8 @@ describe('Agent status service', () => { type: AGENT_TYPE_PERMANENT, attributes: { active: true, - local_metadata: '{}', - user_provided_metadata: '{}', + local_metadata: {}, + user_provided_metadata: {}, }, } as SavedObject); const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.test.ts b/x-pack/plugins/ingest_manager/server/services/datasource.test.ts index 4cbbadce7f5bb..3682ae6d1167b 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/datasource.test.ts @@ -5,39 +5,38 @@ */ import { datasourceService } from './datasource'; +import { PackageInfo } from '../types'; -async function mockedGetAssetsData(_a: any, _b: any, dataset: string) { - if (dataset === 'dataset1') { - return [ - { - buffer: Buffer.from(` +const TEMPLATE = ` type: log metricset: ["dataset1"] paths: {{#each paths}} - {{this}} {{/each}} -`), - }, - ]; - } - return []; -} - -jest.mock('./epm/packages/assets', () => { - return { - getAssetsDataForPackageKey: mockedGetAssetsData, - }; -}); +`; describe('Datasource service', () => { describe('assignPackageStream', () => { - it('should work with cofig variables from the stream', async () => { + it('should work with config variables from the stream', async () => { const inputs = await datasourceService.assignPackageStream( - { - pkgName: 'package', - pkgVersion: '1.0.0', - }, + ({ + datasources: [ + { + inputs: [ + { + type: 'log', + streams: [ + { + dataset: 'package.dataset1', + template: TEMPLATE, + }, + ], + }, + ], + }, + ], + } as unknown) as PackageInfo, [ { type: 'log', @@ -85,10 +84,23 @@ describe('Datasource service', () => { it('should work with config variables at the input level', async () => { const inputs = await datasourceService.assignPackageStream( - { - pkgName: 'package', - pkgVersion: '1.0.0', - }, + ({ + datasources: [ + { + inputs: [ + { + type: 'log', + streams: [ + { + dataset: 'package.dataset1', + template: TEMPLATE, + }, + ], + }, + ], + }, + ], + } as unknown) as PackageInfo, [ { type: 'log', diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.ts b/x-pack/plugins/ingest_manager/server/services/datasource.ts index 804039cf508ba..0a5ba43e40fba 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/services/datasource.ts @@ -4,20 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectsClientContract } from 'src/core/server'; -import { safeLoad } from 'js-yaml'; import { AuthenticatedUser } from '../../../security/server'; import { DeleteDatasourcesResponse, packageToConfigDatasource, DatasourceInput, DatasourceInputStream, + PackageInfo, } from '../../common'; import { DATASOURCE_SAVED_OBJECT_TYPE } from '../constants'; import { NewDatasource, Datasource, ListWithKuery } from '../types'; import { agentConfigService } from './agent_config'; import { getPackageInfo, getInstallation } from './epm/packages'; import { outputService } from './output'; -import { getAssetsDataForPackageKey } from './epm/packages/assets'; import { createStream } from './epm/agent/agent'; const SAVED_OBJECT_TYPE = DATASOURCE_SAVED_OBJECT_TYPE; @@ -201,20 +200,16 @@ class DatasourceService { } public async assignPackageStream( - pkgInfo: { pkgName: string; pkgVersion: string }, + pkgInfo: PackageInfo, inputs: DatasourceInput[] ): Promise { const inputsPromises = inputs.map(input => _assignPackageStreamToInput(pkgInfo, input)); + return Promise.all(inputsPromises); } } -const _isAgentStream = (p: string) => !!p.match(/agent\/stream\/stream\.yml/); - -async function _assignPackageStreamToInput( - pkgInfo: { pkgName: string; pkgVersion: string }, - input: DatasourceInput -) { +async function _assignPackageStreamToInput(pkgInfo: PackageInfo, input: DatasourceInput) { const streamsPromises = input.streams.map(stream => _assignPackageStreamToStream(pkgInfo, input, stream) ); @@ -224,7 +219,7 @@ async function _assignPackageStreamToInput( } async function _assignPackageStreamToStream( - pkgInfo: { pkgName: string; pkgVersion: string }, + pkgInfo: PackageInfo, input: DatasourceInput, stream: DatasourceInputStream ) { @@ -232,27 +227,35 @@ async function _assignPackageStreamToStream( return { ...stream, agent_stream: undefined }; } const dataset = getDataset(stream.dataset); - const assetsData = await getAssetsDataForPackageKey(pkgInfo, _isAgentStream, dataset); + const datasource = pkgInfo.datasources?.[0]; + if (!datasource) { + throw new Error('Stream template not found, no datasource'); + } - const [pkgStream] = assetsData; - if (!pkgStream || !pkgStream.buffer) { - throw new Error(`Stream template not found for dataset ${dataset}`); + const inputFromPkg = datasource.inputs.find(pkgInput => pkgInput.type === input.type); + if (!inputFromPkg) { + throw new Error(`Stream template not found, unable to found input ${input.type}`); } - // Populate template variables from input config and stream config - const data: { [k: string]: string | string[] } = {}; - if (input.vars) { - for (const key of Object.keys(input.vars)) { - data[key] = input.vars[key].value; - } + const streamFromPkg = inputFromPkg.streams.find( + pkgStream => pkgStream.dataset === stream.dataset + ); + if (!streamFromPkg) { + throw new Error(`Stream template not found, unable to found stream ${stream.dataset}`); } - if (stream.vars) { - for (const key of Object.keys(stream.vars)) { - data[key] = stream.vars[key].value; - } + + if (!streamFromPkg.template) { + throw new Error(`Stream template not found for dataset ${dataset}`); } - const yaml = safeLoad(createStream(data, pkgStream.buffer.toString())); + + const yaml = createStream( + // Populate template variables from input vars and stream vars + Object.assign({}, input.vars, stream.vars), + streamFromPkg.template + ); + stream.agent_stream = yaml; + return { ...stream }; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts index 21de625532f03..db2e4fe474640 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts @@ -6,29 +6,61 @@ import { createStream } from './agent'; -test('Test creating a stream from template', () => { - const streamTemplate = ` -input: log -paths: -{{#each paths}} - - {{this}} -{{/each}} -exclude_files: [".gz$"] -processors: - - add_locale: ~ - `; - const vars = { - paths: ['/usr/local/var/log/nginx/access.log'], - }; +describe('createStream', () => { + it('should work', () => { + const streamTemplate = ` + input: log + paths: + {{#each paths}} + - {{this}} + {{/each}} + exclude_files: [".gz$"] + processors: + - add_locale: ~ + `; + const vars = { + paths: { value: ['/usr/local/var/log/nginx/access.log'] }, + }; - const output = createStream(vars, streamTemplate); + const output = createStream(vars, streamTemplate); + expect(output).toEqual({ + input: 'log', + paths: ['/usr/local/var/log/nginx/access.log'], + exclude_files: ['.gz$'], + processors: [{ add_locale: null }], + }); + }); - expect(output).toBe(` -input: log -paths: - - /usr/local/var/log/nginx/access.log -exclude_files: [".gz$"] -processors: - - add_locale: ~ - `); + it('should support yaml values', () => { + const streamTemplate = ` + input: redis/metrics + metricsets: ["key"] + test: null + {{#if key.patterns}} + key.patterns: {{key.patterns}} + {{/if}} + `; + const vars = { + 'key.patterns': { + type: 'yaml', + value: ` + - limit: 20 + pattern: '*' + `, + }, + }; + + const output = createStream(vars, streamTemplate); + expect(output).toEqual({ + input: 'redis/metrics', + metricsets: ['key'], + test: null, + 'key.patterns': [ + { + limit: 20, + pattern: '*', + }, + ], + }); + }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts index 5d9a6d409aa1a..8254c0d8aaa37 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts @@ -5,12 +5,71 @@ */ import Handlebars from 'handlebars'; +import { safeLoad } from 'js-yaml'; +import { DatasourceConfigRecord } from '../../../../common'; -interface StreamVars { - [k: string]: string | string[]; +function isValidKey(key: string) { + return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'; } -export function createStream(vars: StreamVars, streamTemplate: string) { - const template = Handlebars.compile(streamTemplate); - return template(vars); +function replaceVariablesInYaml(yamlVariables: { [k: string]: any }, yaml: any) { + if (Object.keys(yamlVariables).length === 0 || !yaml) { + return yaml; + } + + Object.entries(yaml).forEach(([key, value]: [string, any]) => { + if (typeof value === 'object') { + yaml[key] = replaceVariablesInYaml(yamlVariables, value); + } + if (typeof value === 'string' && value in yamlVariables) { + yaml[key] = yamlVariables[value]; + } + }); + + return yaml; +} + +function buildTemplateVariables(variables: DatasourceConfigRecord) { + const yamlValues: { [k: string]: any } = {}; + const vars = Object.entries(variables).reduce((acc, [key, recordEntry]) => { + // support variables with . like key.patterns + const keyParts = key.split('.'); + const lastKeyPart = keyParts.pop(); + + if (!lastKeyPart || !isValidKey(lastKeyPart)) { + throw new Error('Invalid key'); + } + + let varPart = acc; + for (const keyPart of keyParts) { + if (!isValidKey(keyPart)) { + throw new Error('Invalid key'); + } + if (!varPart[keyPart]) { + varPart[keyPart] = {}; + } + varPart = varPart[keyPart]; + } + + if (recordEntry.type && recordEntry.type === 'yaml') { + const yamlKeyPlaceholder = `##${key}##`; + varPart[lastKeyPart] = `"${yamlKeyPlaceholder}"`; + yamlValues[yamlKeyPlaceholder] = recordEntry.value ? safeLoad(recordEntry.value) : null; + } else { + varPart[lastKeyPart] = recordEntry.value; + } + return acc; + }, {} as { [k: string]: any }); + + return { vars, yamlValues }; +} + +export function createStream(variables: DatasourceConfigRecord, streamTemplate: string) { + const { vars, yamlValues } = buildTemplateVariables(variables); + + const template = Handlebars.compile(streamTemplate, { noEscape: true }); + const stream = template(vars); + const yamlFromStream = safeLoad(stream, {}); + + return replaceVariablesInYaml(yamlValues, yamlFromStream); } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts index 1a73c9581a2de..cacf84381dd88 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts @@ -93,7 +93,7 @@ test('tests processing text field with multi fields', () => { const fields: Field[] = safeLoad(textWithMultiFieldsLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(textWithMultiFieldsMapping)); + expect(mappings).toEqual(textWithMultiFieldsMapping); }); test('tests processing keyword field with multi fields', () => { @@ -127,7 +127,7 @@ test('tests processing keyword field with multi fields', () => { const fields: Field[] = safeLoad(keywordWithMultiFieldsLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(keywordWithMultiFieldsMapping)); + expect(mappings).toEqual(keywordWithMultiFieldsMapping); }); test('tests processing keyword field with multi fields with analyzed text field', () => { @@ -159,7 +159,7 @@ test('tests processing keyword field with multi fields with analyzed text field' const fields: Field[] = safeLoad(keywordWithAnalyzedMultiFieldsLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(keywordWithAnalyzedMultiFieldsMapping)); + expect(mappings).toEqual(keywordWithAnalyzedMultiFieldsMapping); }); test('tests processing object field with no other attributes', () => { @@ -177,7 +177,7 @@ test('tests processing object field with no other attributes', () => { const fields: Field[] = safeLoad(objectFieldLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldMapping)); + expect(mappings).toEqual(objectFieldMapping); }); test('tests processing object field with enabled set to false', () => { @@ -197,7 +197,7 @@ test('tests processing object field with enabled set to false', () => { const fields: Field[] = safeLoad(objectFieldEnabledFalseLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldEnabledFalseMapping)); + expect(mappings).toEqual(objectFieldEnabledFalseMapping); }); test('tests processing object field with dynamic set to false', () => { @@ -217,7 +217,7 @@ test('tests processing object field with dynamic set to false', () => { const fields: Field[] = safeLoad(objectFieldDynamicFalseLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldDynamicFalseMapping)); + expect(mappings).toEqual(objectFieldDynamicFalseMapping); }); test('tests processing object field with dynamic set to true', () => { @@ -237,7 +237,7 @@ test('tests processing object field with dynamic set to true', () => { const fields: Field[] = safeLoad(objectFieldDynamicTrueLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldDynamicTrueMapping)); + expect(mappings).toEqual(objectFieldDynamicTrueMapping); }); test('tests processing object field with dynamic set to strict', () => { @@ -257,5 +257,159 @@ test('tests processing object field with dynamic set to strict', () => { const fields: Field[] = safeLoad(objectFieldDynamicStrictLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldDynamicStrictMapping)); + expect(mappings).toEqual(objectFieldDynamicStrictMapping); +}); + +test('tests processing object field with property', () => { + const objectFieldWithPropertyLiteralYml = ` +- name: a + type: object +- name: a.b + type: keyword + `; + const objectFieldWithPropertyMapping = { + properties: { + a: { + properties: { + b: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldWithPropertyLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(objectFieldWithPropertyMapping); +}); + +test('tests processing object field with property, reverse order', () => { + const objectFieldWithPropertyReversedLiteralYml = ` +- name: a.b + type: keyword +- name: a + type: object + dynamic: false + `; + const objectFieldWithPropertyReversedMapping = { + properties: { + a: { + dynamic: false, + properties: { + b: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldWithPropertyReversedLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(objectFieldWithPropertyReversedMapping); +}); + +test('tests processing nested field with property', () => { + const nestedYaml = ` + - name: a.b + type: keyword + - name: a + type: nested + dynamic: false + `; + const expectedMapping = { + properties: { + a: { + dynamic: false, + type: 'nested', + properties: { + b: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(nestedYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(expectedMapping); +}); + +test('tests processing nested field with property, nested field first', () => { + const nestedYaml = ` + - name: a + type: nested + include_in_parent: true + - name: a.b + type: keyword + `; + const expectedMapping = { + properties: { + a: { + include_in_parent: true, + type: 'nested', + properties: { + b: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(nestedYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(expectedMapping); +}); + +test('tests processing nested leaf field with properties', () => { + const nestedYaml = ` + - name: a + type: object + dynamic: false + - name: a.b + type: nested + enabled: false + `; + const expectedMapping = { + properties: { + a: { + dynamic: false, + properties: { + b: { + enabled: false, + type: 'nested', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(nestedYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(expectedMapping); +}); + +test('tests constant_keyword field type handling', () => { + const constantKeywordLiteralYaml = ` +- name: constantKeyword + type: constant_keyword + `; + const constantKeywordMapping = { + properties: { + constantKeyword: { + type: 'constant_keyword', + }, + }, + }; + const fields: Field[] = safeLoad(constantKeywordLiteralYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(constantKeywordMapping)); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index 9736f6d1cbd3c..c45c7e706be58 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -71,7 +71,14 @@ export function generateMappings(fields: Field[]): IndexTemplateMappings { switch (type) { case 'group': - fieldProps = generateMappings(field.fields!); + fieldProps = { ...generateMappings(field.fields!), ...generateDynamicAndEnabled(field) }; + break; + case 'group-nested': + fieldProps = { + ...generateMappings(field.fields!), + ...generateNestedProps(field), + type: 'nested', + }; break; case 'integer': fieldProps.type = 'long'; @@ -95,13 +102,10 @@ export function generateMappings(fields: Field[]): IndexTemplateMappings { } break; case 'object': - fieldProps.type = 'object'; - if (field.hasOwnProperty('enabled')) { - fieldProps.enabled = field.enabled; - } - if (field.hasOwnProperty('dynamic')) { - fieldProps.dynamic = field.dynamic; - } + fieldProps = { ...fieldProps, ...generateDynamicAndEnabled(field), type: 'object' }; + break; + case 'nested': + fieldProps = { ...fieldProps, ...generateNestedProps(field), type: 'nested' }; break; case 'array': // this assumes array fields were validated in an earlier step @@ -128,6 +132,29 @@ export function generateMappings(fields: Field[]): IndexTemplateMappings { return { properties: props }; } +function generateDynamicAndEnabled(field: Field) { + const props: Properties = {}; + if (field.hasOwnProperty('enabled')) { + props.enabled = field.enabled; + } + if (field.hasOwnProperty('dynamic')) { + props.dynamic = field.dynamic; + } + return props; +} + +function generateNestedProps(field: Field) { + const props = generateDynamicAndEnabled(field); + + if (field.hasOwnProperty('include_in_parent')) { + props.include_in_parent = field.include_in_parent; + } + if (field.hasOwnProperty('include_in_root')) { + props.include_in_root = field.include_in_root; + } + return props; +} + function generateMultiFields(fields: Fields): MultiFields { const multiFields: MultiFields = {}; if (fields) { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts index e3aef6077dbc3..f0ff4c6125452 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts @@ -179,4 +179,197 @@ describe('processFields', () => { JSON.stringify(mixedFieldsExpanded) ); }); + + const objectFieldWithProperty = [ + { + name: 'a', + type: 'object', + dynamic: true, + }, + { + name: 'a.b', + type: 'keyword', + }, + ]; + + const objectFieldWithPropertyExpanded = [ + { + name: 'a', + type: 'group', + dynamic: true, + fields: [ + { + name: 'b', + type: 'keyword', + }, + ], + }, + ]; + test('correctly handles properties of object type fields', () => { + expect(JSON.stringify(processFields(objectFieldWithProperty))).toEqual( + JSON.stringify(objectFieldWithPropertyExpanded) + ); + }); + + test('correctly handles properties of object type fields where object comes second', () => { + const nested = [ + { + name: 'a.b', + type: 'keyword', + }, + { + name: 'a', + type: 'object', + dynamic: true, + }, + ]; + + const nestedExpanded = [ + { + name: 'a', + type: 'group', + dynamic: true, + fields: [ + { + name: 'b', + type: 'keyword', + }, + ], + }, + ]; + expect(processFields(nested)).toEqual(nestedExpanded); + }); + + test('correctly handles properties of nested type fields', () => { + const nested = [ + { + name: 'a', + type: 'nested', + dynamic: true, + }, + { + name: 'a.b', + type: 'keyword', + }, + ]; + + const nestedExpanded = [ + { + name: 'a', + type: 'group-nested', + dynamic: true, + fields: [ + { + name: 'b', + type: 'keyword', + }, + ], + }, + ]; + expect(processFields(nested)).toEqual(nestedExpanded); + }); + + test('correctly handles properties of nested type where nested top level comes second', () => { + const nested = [ + { + name: 'a.b', + type: 'keyword', + }, + { + name: 'a', + type: 'nested', + dynamic: true, + }, + ]; + + const nestedExpanded = [ + { + name: 'a', + type: 'group-nested', + dynamic: true, + fields: [ + { + name: 'b', + type: 'keyword', + }, + ], + }, + ]; + expect(processFields(nested)).toEqual(nestedExpanded); + }); + + test('ignores redefinitions of an object field', () => { + const object = [ + { + name: 'a', + type: 'object', + dynamic: true, + }, + { + name: 'a', + type: 'object', + dynamic: false, + }, + ]; + + const objectExpected = [ + { + name: 'a', + type: 'object', + // should preserve the field that was parsed first which had dynamic: true + dynamic: true, + }, + ]; + expect(processFields(object)).toEqual(objectExpected); + }); + + test('ignores redefinitions of a nested field', () => { + const nested = [ + { + name: 'a', + type: 'nested', + dynamic: true, + }, + { + name: 'a', + type: 'nested', + dynamic: false, + }, + ]; + + const nestedExpected = [ + { + name: 'a', + type: 'nested', + // should preserve the field that was parsed first which had dynamic: true + dynamic: true, + }, + ]; + expect(processFields(nested)).toEqual(nestedExpected); + }); + + test('ignores redefinitions of a nested and object field', () => { + const nested = [ + { + name: 'a', + type: 'nested', + dynamic: true, + }, + { + name: 'a', + type: 'object', + dynamic: false, + }, + ]; + + const nestedExpected = [ + { + name: 'a', + type: 'nested', + // should preserve the field that was parsed first which had dynamic: true + dynamic: true, + }, + ]; + expect(processFields(nested)).toEqual(nestedExpected); + }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts index 9c9843e0454ab..abaf7ab5b0dfc 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts @@ -28,6 +28,8 @@ export interface Field { object_type?: string; scaling_factor?: number; dynamic?: 'strict' | boolean; + include_in_parent?: boolean; + include_in_root?: boolean; // Kibana specific analyzed?: boolean; @@ -108,10 +110,54 @@ function dedupFields(fields: Fields): Fields { return f.name === field.name; }); if (found) { - if (found.type === 'group' && field.type === 'group' && found.fields && field.fields) { - found.fields = dedupFields(found.fields.concat(field.fields)); + // remove name, type, and fields from `field` variable so we avoid merging them into `found` + const { name, type, fields: nestedFields, ...importantFieldProps } = field; + /** + * There are a couple scenarios this if is trying to account for: + * Example 1 + * - name: a.b + * - name: a + * In this scenario found will be `group` and field could be either `object` or `nested` + * Example 2 + * - name: a + * - name: a.b + * In this scenario found could be `object` or `nested` and field will be group + */ + if ( + // only merge if found is a group and field is object, nested, or group. + // Or if found is object, or nested, and field is a group. + // This is to avoid merging two objects, or nested, or object with a nested. + (found.type === 'group' && + (field.type === 'object' || field.type === 'nested' || field.type === 'group')) || + ((found.type === 'object' || found.type === 'nested') && field.type === 'group') + ) { + // if the new field has properties let's dedup and concat them with the already existing found variable in + // the array + if (field.fields) { + // if the found type was object or nested it won't have a fields array so let's initialize it + if (!found.fields) { + found.fields = []; + } + found.fields = dedupFields(found.fields.concat(field.fields)); + } + + // if found already had fields or got new ones from the new field coming in we need to assign the right + // type to it + if (found.fields) { + // If this field is supposed to be `nested` and we have fields, we need to preserve the fact that it is + // supposed to be `nested` for when the template is actually generated + if (found.type === 'nested' || field.type === 'nested') { + found.type = 'group-nested'; + } else { + // found was either `group` already or `object` so just set it to `group` + found.type = 'group'; + } + } + // we need to merge in other properties (like `dynamic`) that might exist + Object.assign(found, importantFieldProps); + // if `field.type` wasn't group object or nested, then there's a conflict in types, so lets ignore it } else { - // only 'group' fields can be merged in this way + // only `group`, `object`, and `nested` fields can be merged in this way // XXX: don't abort on error for now // see discussion in https://github.com/elastic/kibana/pull/59894 // throw new Error( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts index bc1694348b4c2..f1660fbc01591 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts @@ -150,6 +150,7 @@ describe('creating index patterns from yaml fields', () => { { fields: [{ name: 'testField', type: 'text' }], expect: 'string' }, { fields: [{ name: 'testField', type: 'date' }], expect: 'date' }, { fields: [{ name: 'testField', type: 'geo_point' }], expect: 'geo_point' }, + { fields: [{ name: 'testField', type: 'constant_keyword' }], expect: 'string' }, ]; tests.forEach(test => { @@ -191,6 +192,7 @@ describe('creating index patterns from yaml fields', () => { attr: 'aggregatable', }, { fields: [{ name, type: 'keyword' }], expect: true, attr: 'aggregatable' }, + { fields: [{ name, type: 'constant_keyword' }], expect: true, attr: 'aggregatable' }, { fields: [{ name, type: 'text', aggregatable: true }], expect: false, attr: 'aggregatable' }, { fields: [{ name, type: 'text' }], expect: false, attr: 'aggregatable' }, { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts index 05e64c6565dc6..ec657820a2225 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts @@ -47,6 +47,7 @@ const typeMap: TypeMap = { date: 'date', ip: 'ip', boolean: 'boolean', + constant_keyword: 'string', }; export interface IndexPatternField { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index d76584225877c..da8d79a04b97c 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -67,9 +67,10 @@ export async function getPackageInfo(options: { pkgVersion: string; }): Promise { const { savedObjectsClient, pkgName, pkgVersion } = options; - const [item, savedObject, assets] = await Promise.all([ + const [item, savedObject, latestPackage, assets] = await Promise.all([ Registry.fetchInfo(pkgName, pkgVersion), getInstallationObject({ savedObjectsClient, pkgName }), + Registry.fetchFindLatestPackage(pkgName), Registry.getArchiveInfo(pkgName, pkgVersion), ] as const); // adding `as const` due to regression in TS 3.7.2 @@ -79,6 +80,7 @@ export async function getPackageInfo(options: { // add properties that aren't (or aren't yet) on Registry response const updated = { ...item, + latestVersion: latestPackage.version, title: item.title || nameAsTitle(item.name), assets: Registry.groupPathsByService(assets || []), }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index d49e0e661440f..c67cccd044bf5 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -43,6 +43,7 @@ export function createInstallableFrom( ? { ...from, status: InstallationStatus.installed, + installedVersion: savedObject.attributes.version, savedObject, } : { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 06f3decdbbe6f..8f51c4d78305c 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -106,7 +106,7 @@ export async function installPackage(options: { try { await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedPkg.attributes.installed); } catch (err) { - // some assets may not exist if deleting during a failed update + // log these errors, some assets may not exist if deleted during a failed update } } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index ac2c869f3b9e9..befb4722b6504 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -121,8 +121,12 @@ export async function deleteKibanaSavedObjectsAssets( const deletePromises = installedObjects.map(({ id, type }) => { const assetType = type as AssetType; if (savedObjectTypes.includes(assetType)) { - savedObjectsClient.delete(assetType, id); + return savedObjectsClient.delete(assetType, id); } }); - await Promise.all(deletePromises); + try { + await Promise.all(deletePromises); + } catch (err) { + throw new Error('error deleting saved object asset'); + } } diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 206ad76703cf5..390e240841611 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -145,7 +145,7 @@ async function addPackageToConfig( config.namespace ); newDatasource.inputs = await datasourceService.assignPackageStream( - { pkgName: packageToInstall.name, pkgVersion: packageToInstall.version }, + packageInfo, newDatasource.inputs ); diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index a7019ebc0a271..27ed1de849987 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -8,6 +8,7 @@ import { ScopedClusterClient } from 'src/core/server'; export { // Object types Agent, + AgentMetadata, AgentSOAttributes, AgentStatus, AgentType, diff --git a/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts index 040b2eb16289a..59cadf3bd7f74 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts @@ -11,6 +11,9 @@ const AgentConfigBaseSchema = { name: schema.string(), namespace: schema.maybe(schema.string()), description: schema.maybe(schema.string()), + monitoring_enabled: schema.maybe( + schema.arrayOf(schema.oneOf([schema.literal('logs'), schema.literal('metrics')])) + ), }; export const NewAgentConfigSchema = schema.object({ diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 359c06a6a9ebc..21bbcce68bf36 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -41,6 +41,10 @@ export const datatableVisualization: Visualization< }, ], + getVisualizationTypeId() { + return 'lnsDatatable'; + }, + getLayerIds(state) { return state.layers.map(l => l.layerId); }, @@ -122,8 +126,8 @@ export const datatableVisualization: Visualization< ], }, previewIcon: chartTableSVG, - // dont show suggestions for reduced versions or single-line tables - hide: table.changeType === 'reduced' || !table.isMultiRow, + // tables are hidden from suggestion bar, but used for drag & drop and chart switching + hide: true, }, ]; }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx index 3c61d270b1bcf..c8d8064e60e38 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx @@ -62,7 +62,25 @@ describe('chart_switch', () => { id: 'subvisC2', label: 'C2', }, + { + icon: 'empty', + id: 'subvisC3', + label: 'C3', + }, ], + getSuggestions: jest.fn(options => { + if (options.subVisualizationId === 'subvisC2') { + return []; + } + return [ + { + score: 1, + title: '', + state: `suggestion`, + previewIcon: 'empty', + }, + ]; + }), }, }; } @@ -313,10 +331,11 @@ describe('chart_switch', () => { expect(getMenuItem('subvisB', component).prop('betaBadgeIconType')).toBeUndefined(); }); - it('should not indicate data loss if visualization is not changed', () => { + it('should not show a warning when the subvisualization is the same', () => { const dispatch = jest.fn(); const frame = mockFrame(['a', 'b', 'c']); const visualizations = mockVisualizations(); + visualizations.visC.getVisualizationTypeId.mockReturnValue('subvisC2'); const switchVisualizationType = jest.fn(() => 'therebedragons'); visualizations.visC.switchVisualizationType = switchVisualizationType; @@ -333,10 +352,10 @@ describe('chart_switch', () => { /> ); - expect(getMenuItem('subvisC2', component).prop('betaBadgeIconType')).toBeUndefined(); + expect(getMenuItem('subvisC2', component).prop('betaBadgeIconType')).not.toBeDefined(); }); - it('should remove all layers if there is no suggestion', () => { + it('should get suggestions when switching subvisualization', () => { const dispatch = jest.fn(); const visualizations = mockVisualizations(); visualizations.visB.getSuggestions.mockReturnValueOnce([]); @@ -377,7 +396,7 @@ describe('chart_switch', () => { const dispatch = jest.fn(); const frame = mockFrame(['a', 'b', 'c']); const visualizations = mockVisualizations(); - const switchVisualizationType = jest.fn(() => 'therebedragons'); + const switchVisualizationType = jest.fn(() => 'switched'); visualizations.visC.switchVisualizationType = switchVisualizationType; @@ -393,12 +412,12 @@ describe('chart_switch', () => { /> ); - switchTo('subvisC2', component); - expect(switchVisualizationType).toHaveBeenCalledWith('subvisC2', 'therebegriffins'); + switchTo('subvisC3', component); + expect(switchVisualizationType).toHaveBeenCalledWith('subvisC3', 'suggestion'); expect(dispatch).toHaveBeenCalledWith( expect.objectContaining({ type: 'SWITCH_VISUALIZATION', - initialState: 'therebedragons', + initialState: 'switched', }) ); expect(frame.removeLayers).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx index 1461449f3c1c8..d73f83e75c0e4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx @@ -105,7 +105,16 @@ export function ChartSwitch(props: Props) { const switchVisType = props.visualizationMap[visualizationId].switchVisualizationType || ((_type: string, initialState: unknown) => initialState); - if (props.visualizationId === visualizationId) { + const layers = Object.entries(props.framePublicAPI.datasourceLayers); + const containsData = layers.some( + ([_layerId, datasource]) => datasource.getTableSpec().length > 0 + ); + // Always show the active visualization as a valid selection + if ( + props.visualizationId === visualizationId && + props.visualizationState && + newVisualization.getVisualizationTypeId(props.visualizationState) === subVisualizationId + ) { return { visualizationId, subVisualizationId, @@ -116,13 +125,13 @@ export function ChartSwitch(props: Props) { }; } - const layers = Object.entries(props.framePublicAPI.datasourceLayers); - const containsData = layers.some( - ([_layerId, datasource]) => datasource.getTableSpec().length > 0 + const topSuggestion = getTopSuggestion( + props, + visualizationId, + newVisualization, + subVisualizationId ); - const topSuggestion = getTopSuggestion(props, visualizationId, newVisualization); - let dataLoss: VisualizationSelection['dataLoss']; if (!containsData) { @@ -250,7 +259,8 @@ export function ChartSwitch(props: Props) { function getTopSuggestion( props: Props, visualizationId: string, - newVisualization: Visualization + newVisualization: Visualization, + subVisualizationId?: string ): Suggestion | undefined { const suggestions = getSuggestions({ datasourceMap: props.datasourceMap, @@ -258,6 +268,7 @@ function getTopSuggestion( visualizationMap: { [visualizationId]: newVisualization }, activeVisualizationId: props.visualizationId, visualizationState: props.visualizationState, + subVisualizationId, }).filter(suggestion => { // don't use extended versions of current data table on switching between visualizations // to avoid confusing the user. diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index eabcdfa7a24ab..949ae1f43448e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -44,6 +44,7 @@ export function getSuggestions({ datasourceStates, visualizationMap, activeVisualizationId, + subVisualizationId, visualizationState, field, }: { @@ -57,6 +58,7 @@ export function getSuggestions({ >; visualizationMap: Record; activeVisualizationId: string | null; + subVisualizationId?: string; visualizationState: unknown; field?: unknown; }): Suggestion[] { @@ -89,7 +91,8 @@ export function getSuggestions({ table, visualizationId, datasourceSuggestion, - currentVisualizationState + currentVisualizationState, + subVisualizationId ); }) ) @@ -108,13 +111,15 @@ function getVisualizationSuggestions( table: TableSuggestion, visualizationId: string, datasourceSuggestion: DatasourceSuggestion & { datasourceId: string }, - currentVisualizationState: unknown + currentVisualizationState: unknown, + subVisualizationId?: string ) { return visualization .getSuggestions({ table, state: currentVisualizationState, keptLayerIds: datasourceSuggestion.keptLayerIds, + subVisualizationId, }) .map(({ state, ...visualizationSuggestion }) => ({ ...visualizationSuggestion, diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 50cd1ad8bd53a..e684fe8b3b5d6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -28,6 +28,7 @@ export function createMockVisualization(): jest.Mocked { label: 'TEST', }, ], + getVisualizationTypeId: jest.fn(_state => 'empty'), getDescription: jest.fn(_state => ({ label: '' })), switchVisualizationType: jest.fn((_, x) => x), getPersistableState: jest.fn(_state => _state), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts deleted file mode 100644 index 5f35ef650a08c..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts +++ /dev/null @@ -1,83 +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 { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; -import { getAutoDate } from './auto_date'; - -describe('auto_date', () => { - let autoDate: ReturnType; - - beforeEach(() => { - autoDate = getAutoDate({ data: dataPluginMock.createSetupContract() }); - }); - - it('should do nothing if no time range is provided', () => { - const result = autoDate.fn( - { - type: 'kibana_context', - }, - { - aggConfigs: 'canttouchthis', - }, - // eslint-disable-next-line - {} as any - ); - - expect(result).toEqual('canttouchthis'); - }); - - it('should not change anything if there are no auto date histograms', () => { - const aggConfigs = JSON.stringify([ - { type: 'date_histogram', params: { interval: '35h' } }, - { type: 'count' }, - ]); - const result = autoDate.fn( - { - timeRange: { - from: 'now-10d', - to: 'now', - }, - type: 'kibana_context', - }, - { - aggConfigs, - }, - // eslint-disable-next-line - {} as any - ); - - expect(result).toEqual(aggConfigs); - }); - - it('should change auto date histograms', () => { - const aggConfigs = JSON.stringify([ - { type: 'date_histogram', params: { interval: 'auto' } }, - { type: 'count' }, - ]); - const result = autoDate.fn( - { - timeRange: { - from: 'now-10d', - to: 'now', - }, - type: 'kibana_context', - }, - { - aggConfigs, - }, - // eslint-disable-next-line - {} as any - ); - - const interval = JSON.parse(result).find( - (agg: { type: string }) => agg.type === 'date_histogram' - ).params.interval; - - expect(interval).toBeTruthy(); - expect(typeof interval).toEqual('string'); - expect(interval).not.toEqual('auto'); - }); -}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts deleted file mode 100644 index 97a46f4a3e176..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts +++ /dev/null @@ -1,79 +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 { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; -import { - ExpressionFunctionDefinition, - KibanaContext, -} from '../../../../../src/plugins/expressions/public'; - -interface LensAutoDateProps { - aggConfigs: string; -} - -export function getAutoDate(deps: { - data: DataPublicPluginSetup; -}): ExpressionFunctionDefinition< - 'lens_auto_date', - KibanaContext | null, - LensAutoDateProps, - string -> { - function autoIntervalFromContext(ctx?: KibanaContext | null) { - if (!ctx || !ctx.timeRange) { - return; - } - - return deps.data.search.aggs.calculateAutoTimeExpression(ctx.timeRange); - } - - /** - * Convert all 'auto' date histograms into a concrete value (e.g. 2h). - * This allows us to support 'auto' on all date fields, and opens the - * door to future customizations (e.g. adjusting the level of detail, etc). - */ - return { - name: 'lens_auto_date', - aliases: [], - help: '', - inputTypes: ['kibana_context', 'null'], - args: { - aggConfigs: { - types: ['string'], - default: '""', - help: '', - }, - }, - fn(input, args) { - const interval = autoIntervalFromContext(input); - - if (!interval) { - return args.aggConfigs; - } - - const configs = JSON.parse(args.aggConfigs) as Array<{ - type: string; - params: { interval: string }; - }>; - - const updatedConfigs = configs.map(c => { - if (c.type !== 'date_histogram' || !c.params || c.params.interval !== 'auto') { - return c; - } - - return { - ...c, - params: { - ...c.params, - interval, - }, - }; - }); - - return JSON.stringify(updatedConfigs); - }, - }; -} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 074c40759f8d8..9df79aa9e0908 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -1380,5 +1380,43 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }); }); + + it('does not set the size of the terms aggregation', () => { + const dragging = { + field: { type: 'string', name: 'mystring', aggregatable: true }, + indexPatternId: 'foo', + }; + const testState = dragDropState(); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed, + layerId: 'myLayer', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: { + ...testState.layers.myLayer, + columnOrder: ['col1', 'col2'], + columns: { + ...testState.layers.myLayer.columns, + col2: expect.objectContaining({ + operationType: 'terms', + params: expect.objectContaining({ size: 3 }), + }), + }, + }, + }, + }); + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index fe14f472341af..73fd144b9c7f8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -8,7 +8,6 @@ import { CoreSetup } from 'kibana/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { getIndexPatternDatasource } from './indexpattern'; import { renameColumns } from './rename_columns'; -import { getAutoDate } from './auto_date'; import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import { DataPublicPluginSetup, @@ -31,10 +30,9 @@ export class IndexPatternDatasource { setup( core: CoreSetup, - { data: dataSetup, expressions, editorFrame }: IndexPatternDatasourceSetupPlugins + { expressions, editorFrame }: IndexPatternDatasourceSetupPlugins ) { expressions.registerFunction(renameColumns); - expressions.registerFunction(getAutoDate({ data: dataSetup })); editorFrame.registerDatasource( core.getStartServices().then(([coreStart, { data }]) => diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index e4f3677d0fe88..06635e663361d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -10,6 +10,7 @@ import { DatasourcePublicAPI, Operation, Datasource } from '../types'; import { coreMock } from 'src/core/public/mocks'; import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; +import { Ast } from '@kbn/interpreter/common'; jest.mock('./loader'); jest.mock('../id_generator'); @@ -262,20 +263,7 @@ describe('IndexPattern Data Source', () => { Object { "arguments": Object { "aggConfigs": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object { - "aggConfigs": Array [ - "[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]", - ], - }, - "function": "lens_auto_date", - "type": "function", - }, - ], - "type": "expression", - }, + "[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]", ], "includeFormatHints": Array [ true, @@ -289,6 +277,9 @@ describe('IndexPattern Data Source', () => { "partialRows": Array [ false, ], + "timeFields": Array [ + "timestamp", + ], }, "function": "esaggs", "type": "function", @@ -307,6 +298,89 @@ describe('IndexPattern Data Source', () => { } `); }); + + it('should put all time fields used in date_histograms to the esaggs timeFields parameter', async () => { + const queryPersistedState: IndexPatternPersistedState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + col2: { + label: 'Date', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'auto', + }, + }, + col3: { + label: 'Date 2', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'another_datefield', + params: { + interval: 'auto', + }, + }, + }, + }, + }, + }; + + const state = stateFromPersistedState(queryPersistedState); + + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']); + }); + + it('should not put date fields used outside date_histograms to the esaggs timeFields parameter', async () => { + const queryPersistedState: IndexPatternPersistedState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Count of records', + dataType: 'date', + isBucketed: false, + sourceField: 'timefield', + operationType: 'cardinality', + }, + col2: { + label: 'Date', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'auto', + }, + }, + }, + }, + }, + }; + + const state = stateFromPersistedState(queryPersistedState); + + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp']); + expect(ast.chain[0].arguments.timeFields).not.toContain('timefield'); + }); }); describe('#insertLayer', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 2008b326a539c..f26fd39a60c0e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -184,6 +184,7 @@ describe('IndexPattern Data Source suggestions', () => { id2: expect.objectContaining({ operationType: 'terms', sourceField: 'source', + params: expect.objectContaining({ size: 5 }), }), id3: expect.objectContaining({ operationType: 'count', @@ -388,6 +389,7 @@ describe('IndexPattern Data Source suggestions', () => { id1: expect.objectContaining({ operationType: 'terms', sourceField: 'source', + params: expect.objectContaining({ size: 5 }), }), id2: expect.objectContaining({ operationType: 'count', @@ -779,7 +781,7 @@ describe('IndexPattern Data Source suggestions', () => { expect(suggestions[0].table.columns[0].operation.isBucketed).toBeFalsy(); }); - it('appends a terms column on string field', () => { + it('appends a terms column with default size on string field', () => { const initialState = stateWithNonEmptyTables(); const suggestions = getDatasourceSuggestionsForField(initialState, '1', { name: 'dest', @@ -800,6 +802,7 @@ describe('IndexPattern Data Source suggestions', () => { id1: expect.objectContaining({ operationType: 'terms', sourceField: 'dest', + params: expect.objectContaining({ size: 3 }), }), }, }), @@ -1549,6 +1552,62 @@ describe('IndexPattern Data Source suggestions', () => { expect(suggestions[0].table.columns.length).toBe(1); expect(suggestions[0].table.columns[0].operation.label).toBe('Sum of field1'); }); + + it('contains a reordering suggestion when there are exactly 2 buckets', () => { + const initialState = testInitialState(); + const state: IndexPatternPrivateState = { + indexPatternRefs: [], + existingFields: {}, + currentIndexPatternId: '1', + indexPatterns: expectedIndexPatterns, + showEmptyFields: true, + layers: { + first: { + ...initialState.layers.first, + columns: { + id1: { + label: 'Date histogram', + dataType: 'date', + isBucketed: true, + + operationType: 'date_histogram', + sourceField: 'field2', + params: { + interval: 'd', + }, + }, + id2: { + label: 'Top 5', + dataType: 'string', + isBucketed: true, + + operationType: 'terms', + sourceField: 'field1', + params: { size: 5, orderBy: { type: 'alphabetical' }, orderDirection: 'asc' }, + }, + id3: { + label: 'Average of field1', + dataType: 'number', + isBucketed: false, + + operationType: 'avg', + sourceField: 'field1', + }, + }, + columnOrder: ['id1', 'id2', 'id3'], + }, + }, + }; + + const suggestions = getDatasourceSuggestionsFromCurrentState(state); + expect(suggestions).toContainEqual( + expect.objectContaining({ + table: expect.objectContaining({ + changeType: 'reorder', + }), + }) + ); + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 2b3e976a77ea7..487c1bf759fc2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -17,6 +17,7 @@ import { OperationType, } from './operations'; import { operationDefinitions } from './operations/definitions'; +import { TermsIndexPatternColumn } from './operations/definitions/terms'; import { hasField } from './utils'; import { IndexPattern, @@ -232,6 +233,10 @@ function addFieldAsBucketOperation( [newColumnId]: newColumn, }; + if (buckets.length === 0 && operation === 'terms') { + (newColumn as TermsIndexPatternColumn).params.size = 5; + } + const oldDateHistogramIndex = layer.columnOrder.findIndex( columnId => layer.columns[columnId].operationType === 'date_histogram' ); @@ -327,6 +332,9 @@ function createNewLayerWithBucketAggregation( field, suggestedPriority: undefined, }); + if (operation === 'terms') { + (column as TermsIndexPatternColumn).params.size = 5; + } return { indexPatternId: indexPattern.id, @@ -478,7 +486,7 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId layerId, updatedLayer, label: getNestedTitle([layer.columns[secondBucket], layer.columns[firstBucket]]), - changeType: 'extended', + changeType: 'reorder', }); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 3ab51b5fa3f2b..1308fa3b7ca60 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -10,6 +10,7 @@ import { IndexPatternColumn } from './indexpattern'; import { operationDefinitionMap } from './operations'; import { IndexPattern, IndexPatternPrivateState } from './types'; import { OriginalColumn } from './rename_columns'; +import { dateHistogramOperation } from './operations/definitions'; function getExpressionForLayer( indexPattern: IndexPattern, @@ -68,6 +69,12 @@ function getExpressionForLayer( return base; }); + const allDateHistogramFields = Object.values(columns) + .map(column => + column.operationType === dateHistogramOperation.type ? column.sourceField : null + ) + .filter((field): field is string => Boolean(field)); + return { type: 'expression', chain: [ @@ -79,20 +86,8 @@ function getExpressionForLayer( metricsAtAllLevels: [false], partialRows: [false], includeFormatHints: [true], - aggConfigs: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_auto_date', - arguments: { - aggConfigs: [JSON.stringify(aggs)], - }, - }, - ], - }, - ], + timeFields: allDateHistogramFields, + aggConfigs: [JSON.stringify(aggs)], }, }, { diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx index 73b8019a31eaa..04a1c3865f22d 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx @@ -53,6 +53,10 @@ export const metricVisualization: Visualization = { }, ], + getVisualizationTypeId() { + return 'lnsMetric'; + }, + clearLayer(state) { return { ...state, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 181f192520d0d..04efc642793b0 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -103,9 +103,16 @@ export interface TableSuggestion { * * `unchanged` means the table is the same in the currently active configuration * * `reduced` means the table is a reduced version of the currently active table (some columns dropped, but not all of them) * * `extended` means the table is an extended version of the currently active table (added one or multiple additional columns) + * * `reorder` means the table columns have changed order, which change the data as well * * `layers` means the change is a change to the layer structure, not to the table */ -export type TableChangeType = 'initial' | 'unchanged' | 'reduced' | 'extended' | 'layers'; +export type TableChangeType = + | 'initial' + | 'unchanged' + | 'reduced' + | 'extended' + | 'reorder' + | 'layers'; export interface DatasourceSuggestion { state: T; @@ -312,6 +319,10 @@ export interface SuggestionRequest { * The visualization needs to know which table is being suggested */ keptLayerIds: string[]; + /** + * Different suggestions can be generated for each subtype of the visualization + */ + subVisualizationId?: string; } /** @@ -388,6 +399,11 @@ export interface Visualization { * but can register multiple subtypes */ visualizationTypes: VisualizationType[]; + /** + * Return the ID of the current visualization. Used to highlight + * the active subtype of the visualization. + */ + getVisualizationTypeId: (state: T) => string; /** * If the visualization has subtypes, update the subtype in state. */ diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index 5dfae097be834..3a280fb045b06 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -53,6 +53,7 @@ export class XyVisualization { ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme, timeZone: getTimeZone(core.uiSettings), + histogramBarTarget: core.uiSettings.get('histogram:barTarget'), }) ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx index e75e5fe763d6a..8db00aba0e36d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -40,7 +40,7 @@ const createSampleDatatableWithRows = (rows: KibanaDatatableRow[]): KibanaDatata id: 'c', name: 'c', formatHint: { id: 'string' }, - meta: { type: 'date-histogram', aggConfigParams: { interval: '10s' } }, + meta: { type: 'date-histogram', aggConfigParams: { interval: 'auto' } }, }, { id: 'd', name: 'ColD', formatHint: { id: 'string' } }, ], @@ -156,6 +156,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -203,6 +204,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -237,15 +239,17 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); + // real auto interval is 30mins = 1800000 expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` Object { "max": 1546491600000, "min": 1546405200000, - "minInterval": 10000, + "minInterval": 1728000, } `); }); @@ -271,6 +275,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -279,7 +284,7 @@ describe('xy_expression', () => { Object { "max": 1546491600000, "min": 1546405200000, - "minInterval": 10000, + "minInterval": 1728000, } `); }); @@ -307,6 +312,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -350,6 +356,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -383,6 +390,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -398,6 +406,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -414,6 +423,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -430,6 +440,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -472,6 +483,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -510,6 +522,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -527,6 +540,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -547,6 +561,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -565,6 +580,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="CEST" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -582,6 +598,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -606,6 +623,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -624,6 +642,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -684,6 +703,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -878,6 +898,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -894,6 +915,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -910,6 +932,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -927,6 +950,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -943,6 +967,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], accessors: ['a'] }] }} formatFactory={getFormatSpy} chartTheme={{}} + histogramBarTarget={50} timeZone="UTC" executeTriggerActions={executeTriggerActions} /> @@ -963,6 +988,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index d6b6de479acfb..85cf5753befd7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -6,6 +6,7 @@ import React, { useState, useEffect } from 'react'; import ReactDOM from 'react-dom'; +import moment from 'moment'; import { Chart, Settings, @@ -35,8 +36,8 @@ import { XYArgs, SeriesType, visualizationTypes } from './types'; import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart } from './state_helpers'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; -import { parseInterval } from '../../../../../src/plugins/data/common'; import { getExecuteTriggerActions } from './services'; +import { parseInterval } from '../../../../../src/plugins/data/common'; type InferPropType = T extends React.FunctionComponent ? P : T; type SeriesSpec = InferPropType & @@ -58,6 +59,7 @@ type XYChartRenderProps = XYChartProps & { chartTheme: PartialTheme; formatFactory: FormatFactory; timeZone: string; + histogramBarTarget: number; executeTriggerActions: UiActionsStart['executeTriggerActions']; }; @@ -110,6 +112,7 @@ export const xyChart: ExpressionFunctionDefinition< export const getXyChartRenderer = (dependencies: { formatFactory: Promise; chartTheme: PartialTheme; + histogramBarTarget: number; timeZone: string; }): ExpressionRenderDefinition => ({ name: 'lens_xy_chart_renderer', @@ -130,6 +133,7 @@ export const getXyChartRenderer = (dependencies: { formatFactory={formatFactory} chartTheme={dependencies.chartTheme} timeZone={dependencies.timeZone} + histogramBarTarget={dependencies.histogramBarTarget} executeTriggerActions={executeTriggerActions} /> , @@ -169,6 +173,7 @@ export function XYChart({ formatFactory, timeZone, chartTheme, + histogramBarTarget, executeTriggerActions, }: XYChartRenderProps) { const { legend, layers } = args; @@ -212,18 +217,26 @@ export function XYChart({ const xTitle = (xAxisColumn && xAxisColumn.name) || args.xTitle; - // add minInterval only for single row value as it cannot be determined from dataset + function calculateMinInterval() { + // add minInterval only for single row value as it cannot be determined from dataset + if (data.dateRange && layers.every(layer => data.tables[layer.layerId].rows.length <= 1)) { + if (xAxisColumn?.meta?.aggConfigParams?.interval !== 'auto') + return parseInterval(xAxisColumn?.meta?.aggConfigParams?.interval)?.asMilliseconds(); - const minInterval = layers.every(layer => data.tables[layer.layerId].rows.length <= 1) - ? parseInterval(xAxisColumn?.meta?.aggConfigParams?.interval)?.asMilliseconds() - : undefined; + const { fromDate, toDate } = data.dateRange; + const duration = moment(toDate).diff(moment(fromDate)); + const targetMs = duration / histogramBarTarget; + return isNaN(targetMs) ? 0 : Math.max(Math.floor(targetMs), 1); + } + return undefined; + } const xDomain = data.dateRange && layers.every(l => l.xScaleType === 'time') ? { min: data.dateRange.fromDate.getTime(), max: data.dateRange.toDate.getTime(), - minInterval, + minInterval: calculateMinInterval(), } : undefined; return ( diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index ddbd9d11b5fad..73ff88e97f479 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -186,7 +186,7 @@ describe('xy_suggestions', () => { isMultiRow: true, columns: [numCol('price'), numCol('quantity'), dateCol('date'), strCol('product')], layerId: 'first', - changeType: 'unchanged', + changeType: 'extended', label: 'Datasource title', }, keptLayerIds: [], @@ -196,6 +196,34 @@ describe('xy_suggestions', () => { expect(suggestion.title).toEqual('Datasource title'); }); + test('suggests only stacked bar chart when xy chart is inactive', () => { + const [suggestion, ...rest] = getSuggestions({ + table: { + isMultiRow: true, + columns: [dateCol('date'), numCol('price')], + layerId: 'first', + changeType: 'unchanged', + label: 'Datasource title', + }, + keptLayerIds: [], + }); + + expect(rest).toHaveLength(0); + expect(suggestion.title).toEqual('Bar chart'); + expect(suggestion.state).toEqual( + expect.objectContaining({ + layers: [ + expect.objectContaining({ + seriesType: 'bar_stacked', + xAccessor: 'date', + accessors: ['price'], + splitAccessor: undefined, + }), + ], + }) + ); + }); + test('hides reduced suggestions if there is a current state', () => { const [suggestion, ...rest] = getSuggestions({ table: { @@ -224,7 +252,7 @@ describe('xy_suggestions', () => { expect(suggestion.hide).toBeTruthy(); }); - test('does not hide reduced suggestions if xy visualization is not active', () => { + test('hides reduced suggestions if xy visualization is not active', () => { const [suggestion, ...rest] = getSuggestions({ table: { isMultiRow: true, @@ -236,7 +264,7 @@ describe('xy_suggestions', () => { }); expect(rest).toHaveLength(0); - expect(suggestion.hide).toBeFalsy(); + expect(suggestion.hide).toBeTruthy(); }); test('only makes a seriesType suggestion for unchanged table without split', () => { @@ -419,6 +447,44 @@ describe('xy_suggestions', () => { }); }); + test('changes column mappings when suggestion is reorder', () => { + const currentState: XYState = { + legend: { isVisible: true, position: 'bottom' }, + preferredSeriesType: 'bar', + layers: [ + { + accessors: ['price'], + layerId: 'first', + seriesType: 'bar', + splitAccessor: 'category', + xAccessor: 'product', + }, + ], + }; + const [suggestion, ...rest] = getSuggestions({ + table: { + isMultiRow: true, + columns: [strCol('category'), strCol('product'), numCol('price')], + layerId: 'first', + changeType: 'reorder', + }, + state: currentState, + keptLayerIds: [], + }); + + expect(rest).toHaveLength(0); + expect(suggestion.state).toEqual({ + ...currentState, + layers: [ + { + ...currentState.layers[0], + xAccessor: 'category', + splitAccessor: 'product', + }, + ], + }); + }); + test('overwrites column to dimension mappings if a date dimension is added', () => { (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); const currentState: XYState = { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index 5e9311bb1e928..abd7640344064 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -99,11 +99,14 @@ function getBucketMappings(table: TableSuggestion, currentState?: State) { // reverse the buckets before prioritization to always use the most inner // bucket of the highest-prioritized group as x value (don't use nested // buckets as split series) - const prioritizedBuckets = prioritizeColumns(buckets.reverse()); + const prioritizedBuckets = prioritizeColumns([...buckets].reverse()); if (!currentLayer || table.changeType === 'initial') { return prioritizedBuckets; } + if (table.changeType === 'reorder') { + return buckets; + } // if existing table is just modified, try to map buckets to the current dimensions const currentXColumnIndex = prioritizedBuckets.findIndex( @@ -175,12 +178,24 @@ function getSuggestionsForLayer({ keptLayerIds, }; - const isSameState = currentState && changeType === 'unchanged'; + // handles the simplest cases, acting as a chart switcher + if (!currentState && changeType === 'unchanged') { + return [ + { + ...buildSuggestion(options), + title: i18n.translate('xpack.lens.xySuggestions.barChartTitle', { + defaultMessage: 'Bar chart', + }), + }, + ]; + } + const isSameState = currentState && changeType === 'unchanged'; if (!isSameState) { return buildSuggestion(options); } + // Suggestions are either changing the data, or changing the way the data is used const sameStateSuggestions: Array> = []; // if current state is using the same data, suggest same chart with different presentational configuration @@ -374,8 +389,11 @@ function buildSuggestion({ return { title, score: getScore(yValues, splitBy, changeType), - // don't advertise chart of same type but with less data - hide: currentState && changeType === 'reduced', + hide: + // Only advertise very clear changes when XY chart is not active + (!currentState && changeType !== 'unchanged' && changeType !== 'extended') || + // Don't advertise removing dimensions + (currentState && changeType === 'reduced'), state, previewIcon: getIconForSeries(seriesType), }; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts index beccf0dc46eb4..d176905c65120 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -27,7 +27,7 @@ function exampleState(): State { } describe('xy_visualization', () => { - describe('getDescription', () => { + describe('#getDescription', () => { function mixedState(...types: SeriesType[]) { const state = exampleState(); return { @@ -81,6 +81,45 @@ describe('xy_visualization', () => { }); }); + describe('#getVisualizationTypeId', () => { + function mixedState(...types: SeriesType[]) { + const state = exampleState(); + return { + ...state, + layers: types.map((t, i) => ({ + ...state.layers[0], + layerId: `layer_${i}`, + seriesType: t, + })), + }; + } + + it('should show mixed when each layer is different', () => { + expect(xyVisualization.getVisualizationTypeId(mixedState('bar', 'line'))).toEqual('mixed'); + }); + + it('should show the preferredSeriesType if there are no layers', () => { + expect(xyVisualization.getVisualizationTypeId(mixedState())).toEqual('bar'); + }); + + it('should combine multiple layers into one type', () => { + expect( + xyVisualization.getVisualizationTypeId(mixedState('bar_horizontal', 'bar_horizontal')) + ).toEqual('bar_horizontal'); + }); + + it('should return the subtype for single layers', () => { + expect(xyVisualization.getVisualizationTypeId(mixedState('area'))).toEqual('area'); + expect(xyVisualization.getVisualizationTypeId(mixedState('line'))).toEqual('line'); + expect(xyVisualization.getVisualizationTypeId(mixedState('area_stacked'))).toEqual( + 'area_stacked' + ); + expect(xyVisualization.getVisualizationTypeId(mixedState('bar_horizontal_stacked'))).toEqual( + 'bar_horizontal_stacked' + ); + }); + }); + describe('#initialize', () => { it('loads default state', () => { const mockFrame = createMockFramePublicAPI(); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx index c72fa0fec24d7..e91edf9cc0183 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -12,7 +12,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { getSuggestions } from './xy_suggestions'; import { LayerContextMenu } from './xy_config_panel'; -import { Visualization, OperationMetadata } from '../types'; +import { Visualization, OperationMetadata, VisualizationType } from '../types'; import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types'; import { toExpression, toPreviewExpression } from './to_expression'; import chartBarStackedSVG from '../assets/chart_bar_stacked.svg'; @@ -24,6 +24,18 @@ const defaultSeriesType = 'bar_stacked'; const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; const isBucketed = (op: OperationMetadata) => op.isBucketed; +function getVisualizationType(state: State): VisualizationType | 'mixed' { + if (!state.layers.length) { + return ( + visualizationTypes.find(t => t.id === state.preferredSeriesType) ?? visualizationTypes[0] + ); + } + const visualizationType = visualizationTypes.find(t => t.id === state.layers[0].seriesType); + const seriesTypes = _.unique(state.layers.map(l => l.seriesType)); + + return visualizationType && seriesTypes.length === 1 ? visualizationType : 'mixed'; +} + function getDescription(state?: State) { if (!state) { return { @@ -34,32 +46,31 @@ function getDescription(state?: State) { }; } + const visualizationType = getVisualizationType(state); + if (!state.layers.length) { - const visualizationType = visualizationTypes.find(v => v.id === state.preferredSeriesType)!; + const preferredType = visualizationType as VisualizationType; return { - icon: visualizationType.largeIcon || visualizationType.icon, - label: visualizationType.label, + icon: preferredType.largeIcon || preferredType.icon, + label: preferredType.label, }; } - const visualizationType = visualizationTypes.find(t => t.id === state.layers[0].seriesType)!; - const seriesTypes = _.unique(state.layers.map(l => l.seriesType)); - return { icon: - seriesTypes.length === 1 - ? visualizationType.largeIcon || visualizationType.icon - : chartMixedSVG, + visualizationType === 'mixed' + ? chartMixedSVG + : visualizationType.largeIcon || visualizationType.icon, label: - seriesTypes.length === 1 - ? visualizationType.label - : isHorizontalChart(state.layers) - ? i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', { - defaultMessage: 'Mixed horizontal bar', - }) - : i18n.translate('xpack.lens.xyVisualization.mixedLabel', { - defaultMessage: 'Mixed XY', - }), + visualizationType === 'mixed' + ? isHorizontalChart(state.layers) + ? i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', { + defaultMessage: 'Mixed horizontal bar', + }) + : i18n.translate('xpack.lens.xyVisualization.mixedLabel', { + defaultMessage: 'Mixed XY', + }) + : visualizationType.label, }; } @@ -67,6 +78,10 @@ export const xyVisualization: Visualization = { id: 'lnsXY', visualizationTypes, + getVisualizationTypeId(state) { + const type = getVisualizationType(state); + return type === 'mixed' ? type : type.id; + }, getLayerIds(state) { return state.layers.map(l => l.layerId); diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations.test.ts index e80308cc9acdb..4cc330d40efd7 100644 --- a/x-pack/plugins/lens/server/migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations.test.ts @@ -158,4 +158,124 @@ describe('Lens migrations', () => { ]); }); }); + + describe('7.8.0 auto timestamp', () => { + const context = {} as SavedObjectMigrationContext; + + const example = { + type: 'lens', + attributes: { + expression: `kibana + | kibana_context query="{\\"query\\":\\"\\",\\"language\\":\\"kuery\\"}" filters="[]" + | lens_merge_tables layerIds="bd09dc71-a7e2-42d0-83bd-85df8291f03c" + tables={esaggs + index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" + metricsAtAllLevels=false + partialRows=false + includeFormatHints=true + aggConfigs={ + lens_auto_date + aggConfigs="[{\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"products.created_on\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"auto\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}},{\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" + } + | lens_rename_columns idMap="{\\"col-0-1d9cc16c-1460-41de-88f8-471932ecbc97\\":{\\"label\\":\\"products.created_on\\",\\"dataType\\":\\"date\\",\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"products.created_on\\",\\"isBucketed\\":true,\\"scale\\":\\"interval\\",\\"params\\":{\\"interval\\":\\"auto\\"},\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\"},\\"col-1-66115819-8481-4917-a6dc-8ffb10dd02df\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"suggestedPriority\\":0,\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\"}}" + } + | lens_xy_chart + xTitle="products.created_on" + yTitle="Count of records" + legend={lens_xy_legendConfig isVisible=true position="right"} + layers={lens_xy_layer + layerId="bd09dc71-a7e2-42d0-83bd-85df8291f03c" + hide=false + xAccessor="1d9cc16c-1460-41de-88f8-471932ecbc97" + yScaleType="linear" + xScaleType="time" + isHistogram=true + seriesType="bar_stacked" + accessors="66115819-8481-4917-a6dc-8ffb10dd02df" + columnToLabel="{\\"66115819-8481-4917-a6dc-8ffb10dd02df\\":\\"Count of records\\"}" + } + `, + state: { + datasourceStates: { + indexpattern: { + currentIndexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + layers: { + 'bd09dc71-a7e2-42d0-83bd-85df8291f03c': { + indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + columns: { + '1d9cc16c-1460-41de-88f8-471932ecbc97': { + label: 'products.created_on', + dataType: 'date', + operationType: 'date_histogram', + sourceField: 'products.created_on', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '66115819-8481-4917-a6dc-8ffb10dd02df': { + label: 'Count of records', + dataType: 'number', + operationType: 'count', + suggestedPriority: 0, + isBucketed: false, + scale: 'ratio', + sourceField: 'Records', + }, + }, + columnOrder: [ + '1d9cc16c-1460-41de-88f8-471932ecbc97', + '66115819-8481-4917-a6dc-8ffb10dd02df', + ], + }, + }, + }, + }, + datasourceMetaData: { + filterableIndexPatterns: [ + { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', title: 'kibana_sample_data_ecommerce' }, + ], + }, + visualization: { + legend: { isVisible: true, position: 'right' }, + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId: 'bd09dc71-a7e2-42d0-83bd-85df8291f03c', + accessors: ['66115819-8481-4917-a6dc-8ffb10dd02df'], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + xAccessor: '1d9cc16c-1460-41de-88f8-471932ecbc97', + }, + ], + }, + query: { query: '', language: 'kuery' }, + filters: [], + }, + title: 'Bar chart', + visualizationType: 'lnsXY', + }, + }; + + it('should remove the lens_auto_date expression', () => { + const result = migrations['7.8.0'](example, context); + expect(result.attributes.expression).toContain(`timeFields=\"products.created_on\"`); + }); + + it('should handle pre-migrated expression', () => { + const input = { + type: 'lens', + attributes: { + ...example.attributes, + expression: `kibana +| kibana_context query="{\\"query\\":\\"\\",\\"language\\":\\"kuery\\"}" filters="[]" +| lens_merge_tables layerIds="bd09dc71-a7e2-42d0-83bd-85df8291f03c" + tables={esaggs index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" metricsAtAllLevels=false partialRows=false includeFormatHints=true aggConfigs="[{\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"products.created_on\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"auto\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}},{\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" timeFields=\"products.created_on\"} +| lens_xy_chart xTitle="products.created_on" yTitle="Count of records" legend={lens_xy_legendConfig isVisible=true position="right"} layers={}`, + }, + }; + const result = migrations['7.8.0'](input, context); + expect(result).toEqual(input); + }); + }); }); diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts index 3d238723b7438..583fba1a4a999 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cloneDeep } from 'lodash'; +import { cloneDeep, flow } from 'lodash'; +import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter/common'; import { SavedObjectMigrationFn } from 'src/core/server'; interface XYLayerPre77 { @@ -14,7 +15,126 @@ interface XYLayerPre77 { accessors: string[]; } -export const migrations: Record = { +/** + * Removes the `lens_auto_date` subexpression from a stored expression + * string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"} + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => { + const expression: string = doc.attributes?.expression; + try { + const ast = fromExpression(expression); + const newChain: ExpressionFunctionAST[] = ast.chain.map(topNode => { + if (topNode.function !== 'lens_merge_tables') { + return topNode; + } + return { + ...topNode, + arguments: { + ...topNode.arguments, + tables: (topNode.arguments.tables as Ast[]).map(middleNode => { + return { + type: 'expression', + chain: middleNode.chain.map(node => { + // Check for sub-expression in aggConfigs + if ( + node.function === 'esaggs' && + typeof node.arguments.aggConfigs[0] !== 'string' + ) { + return { + ...node, + arguments: { + ...node.arguments, + aggConfigs: (node.arguments.aggConfigs[0] as Ast).chain[0].arguments + .aggConfigs, + }, + }; + } + return node; + }), + }; + }), + }, + }; + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + expression: toExpression({ ...ast, chain: newChain }), + }, + }; + } catch (e) { + context.log.warning(e.message); + return { ...doc }; + } +}; + +/** + * Adds missing timeField arguments to esaggs in the Lens expression + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => { + const expression: string = doc.attributes?.expression; + + try { + const ast = fromExpression(expression); + const newChain: ExpressionFunctionAST[] = ast.chain.map(topNode => { + if (topNode.function !== 'lens_merge_tables') { + return topNode; + } + return { + ...topNode, + arguments: { + ...topNode.arguments, + tables: (topNode.arguments.tables as Ast[]).map(middleNode => { + return { + type: 'expression', + chain: middleNode.chain.map(node => { + // Skip if there are any timeField arguments already, because that indicates + // the fix is already applied + if (node.function !== 'esaggs' || node.arguments.timeFields) { + return node; + } + const timeFields: string[] = []; + JSON.parse(node.arguments.aggConfigs[0] as string).forEach( + (agg: { type: string; params: { field: string } }) => { + if (agg.type !== 'date_histogram') { + return; + } + timeFields.push(agg.params.field); + } + ); + return { + ...node, + arguments: { + ...node.arguments, + timeFields, + }, + }; + }), + }; + }), + }, + }; + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + expression: toExpression({ ...ast, chain: newChain }), + }, + }; + } catch (e) { + context.log.warning(e.message); + return { ...doc }; + } +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const migrations: Record> = { '7.7.0': doc => { const newDoc = cloneDeep(doc); if (newDoc.attributes?.visualizationType === 'lnsXY') { @@ -34,4 +154,7 @@ export const migrations: Record = { } return newDoc; }, + // The order of these migrations matter, since the timefield migration relies on the aggConfigs + // sitting directly on the esaggs as an argument and not a nested function (which lens_auto_date was). + '7.8.0': flow(removeLensAutoDate, addTimeFieldToEsaggs), }; diff --git a/x-pack/plugins/lists/common/constants.ts b/x-pack/plugins/lists/common/constants.ts new file mode 100644 index 0000000000000..dbe31fed66413 --- /dev/null +++ b/x-pack/plugins/lists/common/constants.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +/** + * Lists routes + */ +export const LIST_URL = `/api/lists`; +export const LIST_INDEX = `${LIST_URL}/index`; +export const LIST_ITEM_URL = `${LIST_URL}/items`; diff --git a/x-pack/legacy/plugins/actions/index.ts b/x-pack/plugins/lists/common/schemas/common/index.ts similarity index 90% rename from x-pack/legacy/plugins/actions/index.ts rename to x-pack/plugins/lists/common/schemas/common/index.ts index 276d1ea3accea..a05e97ded38ee 100644 --- a/x-pack/legacy/plugins/actions/index.ts +++ b/x-pack/plugins/lists/common/schemas/common/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './server/'; +export * from './schemas'; diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts new file mode 100644 index 0000000000000..edc037ed7a0b1 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -0,0 +1,62 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { NonEmptyString } from '../types/non_empty_string'; + +export const name = t.string; +export type Name = t.TypeOf; +export const nameOrUndefined = t.union([name, t.undefined]); +export type NameOrUndefined = t.TypeOf; + +export const description = t.string; +export type Description = t.TypeOf; +export const descriptionOrUndefined = t.union([description, t.undefined]); +export type DescriptionOrUndefined = t.TypeOf; + +export const list_id = NonEmptyString; +export const list_idOrUndefined = t.union([list_id, t.undefined]); +export type List_idOrUndefined = t.TypeOf; + +export const item = t.string; +export const created_at = t.string; // TODO: Make this into an ISO Date string check +export const updated_at = t.string; // TODO: Make this into an ISO Date string check +export const updated_by = t.string; +export const created_by = t.string; +export const file = t.object; + +export const id = NonEmptyString; +export type Id = t.TypeOf; +export const idOrUndefined = t.union([id, t.undefined]); +export type IdOrUndefined = t.TypeOf; + +export const ip = t.string; +export const ipOrUndefined = t.union([ip, t.undefined]); + +export const keyword = t.string; +export const keywordOrUndefined = t.union([keyword, t.undefined]); + +export const value = t.string; +export const valueOrUndefined = t.union([value, t.undefined]); + +export const tie_breaker_id = t.string; // TODO: Use UUID for this instead of a string for validation +export const _index = t.string; + +export const type = t.keyof({ ip: null, keyword: null }); // TODO: Add the other data types here + +export const typeOrUndefined = t.union([type, t.undefined]); +export type Type = t.TypeOf; + +export const meta = t.object; +export type Meta = t.TypeOf; +export const metaOrUndefined = t.union([meta, t.undefined]); +export type MetaOrUndefined = t.TypeOf; + +export const esDataTypeUnion = t.union([t.type({ ip }), t.type({ keyword })]); +export type EsDataTypeUnion = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/create_es_bulk_type.ts b/x-pack/plugins/lists/common/schemas/elastic_query/create_es_bulk_type.ts new file mode 100644 index 0000000000000..4a825382c06e4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/create_es_bulk_type.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 * as t from 'io-ts'; + +import { _index } from '../common/schemas'; + +export const createEsBulkTypeSchema = t.exact( + t.type({ + create: t.exact(t.type({ _index })), + }) +); + +export type CreateEsBulkTypeSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index.ts b/x-pack/plugins/lists/common/schemas/elastic_query/index.ts new file mode 100644 index 0000000000000..d70dd09849fa6 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/index.ts @@ -0,0 +1,10 @@ +/* + * 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 * from './update_es_list_schema'; +export * from './index_es_list_schema'; +export * from './update_es_list_item_schema'; +export * from './index_es_list_item_schema'; +export * from './create_es_bulk_type'; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts new file mode 100644 index 0000000000000..596498b64b771 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + created_at, + created_by, + esDataTypeUnion, + list_id, + metaOrUndefined, + tie_breaker_id, + updated_at, + updated_by, +} from '../common/schemas'; + +export const indexEsListItemSchema = t.intersection([ + t.exact( + t.type({ + created_at, + created_by, + list_id, + meta: metaOrUndefined, + tie_breaker_id, + updated_at, + updated_by, + }) + ), + esDataTypeUnion, +]); + +export type IndexEsListItemSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts new file mode 100644 index 0000000000000..e0924392628a9 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + created_at, + created_by, + description, + metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, +} from '../common/schemas'; + +export const indexEsListSchema = t.exact( + t.type({ + created_at, + created_by, + description, + meta: metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, + }) +); + +export type IndexEsListSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts new file mode 100644 index 0000000000000..e4cf46bc39429 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { esDataTypeUnion, metaOrUndefined, updated_at, updated_by } from '../common/schemas'; + +export const updateEsListItemSchema = t.intersection([ + t.exact( + t.type({ + meta: metaOrUndefined, + updated_at, + updated_by, + }) + ), + esDataTypeUnion, +]); + +export type UpdateEsListItemSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts new file mode 100644 index 0000000000000..8f23f3744e563 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + descriptionOrUndefined, + metaOrUndefined, + nameOrUndefined, + updated_at, + updated_by, +} from '../common/schemas'; + +export const updateEsListSchema = t.exact( + t.type({ + description: descriptionOrUndefined, + meta: metaOrUndefined, + name: nameOrUndefined, + updated_at, + updated_by, + }) +); + +export type UpdateEsListSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/index.ts b/x-pack/plugins/lists/common/schemas/elastic_response/index.ts new file mode 100644 index 0000000000000..6fbc6ef293064 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_response/index.ts @@ -0,0 +1,8 @@ +/* + * 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 * from './search_es_list_item_schema'; +export * from './search_es_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.ts new file mode 100644 index 0000000000000..902d3e6a9896e --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.ts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + created_at, + created_by, + ipOrUndefined, + keywordOrUndefined, + list_id, + metaOrUndefined, + tie_breaker_id, + updated_at, + updated_by, +} from '../common/schemas'; + +export const searchEsListItemSchema = t.exact( + t.type({ + created_at, + created_by, + ip: ipOrUndefined, + keyword: keywordOrUndefined, + list_id, + meta: metaOrUndefined, + tie_breaker_id, + updated_at, + updated_by, + }) +); + +export type SearchEsListItemSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.ts new file mode 100644 index 0000000000000..00a7c6f321d38 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.ts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + created_at, + created_by, + description, + metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, +} from '../common/schemas'; + +export const searchEsListSchema = t.exact( + t.type({ + created_at, + created_by, + description, + meta: metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, + }) +); + +export type SearchEsListSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/index.ts b/x-pack/plugins/lists/common/schemas/index.ts new file mode 100644 index 0000000000000..6a60a6df55691 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/index.ts @@ -0,0 +1,11 @@ +/* + * 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 * from './common'; +export * from './request'; +export * from './response'; +export * from './elastic_query'; +export * from './elastic_response'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts new file mode 100644 index 0000000000000..8168e5a9838f2 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { idOrUndefined, list_id, metaOrUndefined, value } from '../common/schemas'; + +export const createListItemSchema = t.exact( + t.type({ + id: idOrUndefined, + list_id, + meta: metaOrUndefined, + value, + }) +); + +export type CreateListItemSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts new file mode 100644 index 0000000000000..ba791a55d17eb --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getListRequest } from './mocks/utils'; +import { createListSchema } from './create_list_schema'; + +describe('create_list_schema', () => { + // TODO: Finish the tests for this + test('it should validate a typical lists request', () => { + const payload = getListRequest(); + const decoded = createListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + description: 'Description of a list item', + id: 'some-list-id', + name: 'Name of a list item', + type: 'ip', + }); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts new file mode 100644 index 0000000000000..353a4ecdafa0c --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { description, idOrUndefined, metaOrUndefined, name, type } from '../common/schemas'; + +export const createListSchema = t.exact( + t.type({ + description, + id: idOrUndefined, + meta: metaOrUndefined, + name, + type, + }) +); + +export type CreateListSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts new file mode 100644 index 0000000000000..f4c1fb5c43eb0 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { idOrUndefined, list_idOrUndefined, valueOrUndefined } from '../common/schemas'; + +export const deleteListItemSchema = t.exact( + t.type({ + id: idOrUndefined, + list_id: list_idOrUndefined, + value: valueOrUndefined, + }) +); + +export type DeleteListItemSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts new file mode 100644 index 0000000000000..fd6aa5b85f81a --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { id } from '../common/schemas'; + +export const deleteListSchema = t.exact( + t.type({ + id, + }) +); + +export type DeleteListSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts new file mode 100644 index 0000000000000..14b201bf8089d --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { list_id } from '../common/schemas'; + +export const exportListItemQuerySchema = t.exact( + t.type({ + list_id, + // TODO: Add file_name here with a default value + }) +); + +export type ExportListItemQuerySchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts new file mode 100644 index 0000000000000..b8467d141bdd8 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { list_idOrUndefined, typeOrUndefined } from '../common/schemas'; + +export const importListItemQuerySchema = t.exact( + t.type({ list_id: list_idOrUndefined, type: typeOrUndefined }) +); + +export type ImportListItemQuerySchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts new file mode 100644 index 0000000000000..0cf01db8617f0 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import { Readable } from 'stream'; + +import * as t from 'io-ts'; + +import { file } from '../common/schemas'; + +export const importListItemSchema = t.exact( + t.type({ + file, + }) +); + +export interface HapiReadableStream extends Readable { + hapi: { + filename: string; + }; +} + +/** + * Special interface since we are streaming in a file through a reader + */ +export interface ImportListItemSchema { + file: HapiReadableStream; +} diff --git a/x-pack/plugins/lists/common/schemas/request/index.ts b/x-pack/plugins/lists/common/schemas/request/index.ts new file mode 100644 index 0000000000000..d332ab1eb1bab --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/index.ts @@ -0,0 +1,19 @@ +/* + * 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 * from './create_list_item_schema'; +export * from './create_list_schema'; +export * from './delete_list_item_schema'; +export * from './delete_list_schema'; +export * from './export_list_item_query_schema'; +export * from './import_list_item_schema'; +export * from './patch_list_item_schema'; +export * from './patch_list_schema'; +export * from './read_list_item_schema'; +export * from './read_list_schema'; +export * from './import_list_item_query_schema'; +export * from './update_list_schema'; +export * from './update_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/mocks/utils.ts b/x-pack/plugins/lists/common/schemas/request/mocks/utils.ts new file mode 100644 index 0000000000000..e5d189db8490b --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/mocks/utils.ts @@ -0,0 +1,15 @@ +/* + * 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 { CreateListSchema } from '../create_list_schema'; + +export const getListRequest = (): CreateListSchema => ({ + description: 'Description of a list item', + id: 'some-list-id', + meta: undefined, + name: 'Name of a list item', + type: 'ip', +}); diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts new file mode 100644 index 0000000000000..3e8198a5109b3 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { id, metaOrUndefined, valueOrUndefined } from '../common/schemas'; + +export const patchListItemSchema = t.exact( + t.type({ + id, + meta: metaOrUndefined, + value: valueOrUndefined, + }) +); + +export type PatchListItemSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts new file mode 100644 index 0000000000000..efcb81fc8be2a --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { descriptionOrUndefined, id, metaOrUndefined, nameOrUndefined } from '../common/schemas'; + +export const patchListSchema = t.exact( + t.type({ + description: descriptionOrUndefined, + id, + meta: metaOrUndefined, + name: nameOrUndefined, + }) +); + +export type PatchListSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts new file mode 100644 index 0000000000000..9ea14a2a21ed8 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { idOrUndefined, list_idOrUndefined, valueOrUndefined } from '../common/schemas'; + +export const readListItemSchema = t.exact( + t.type({ id: idOrUndefined, list_id: list_idOrUndefined, value: valueOrUndefined }) +); + +export type ReadListItemSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts new file mode 100644 index 0000000000000..8803346709c31 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { id } from '../common/schemas'; + +export const readListSchema = t.exact( + t.type({ + id, + }) +); + +export type ReadListSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts new file mode 100644 index 0000000000000..e1f88bae66e0f --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { id, metaOrUndefined, value } from '../common/schemas'; + +export const updateListItemSchema = t.exact( + t.type({ + id, + meta: metaOrUndefined, + value, + }) +); + +export type UpdateListItemSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts new file mode 100644 index 0000000000000..d51ed60c41b56 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { description, id, metaOrUndefined, name } from '../common/schemas'; + +export const updateListSchema = t.exact( + t.type({ + description, + id, + meta: metaOrUndefined, + name, + }) +); + +export type UpdateListSchema = t.TypeOf; diff --git a/x-pack/legacy/plugins/uptime/common/graphql/resolver_types.ts b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.ts similarity index 57% rename from x-pack/legacy/plugins/uptime/common/graphql/resolver_types.ts rename to x-pack/plugins/lists/common/schemas/response/acknowledge_schema.ts index 22df610d2d516..55aaf587ac06b 100644 --- a/x-pack/legacy/plugins/uptime/common/graphql/resolver_types.ts +++ b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.ts @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -type UMResolverResult = Promise | T; +import * as t from 'io-ts'; -export type UMResolver = ( - parent: Parent, - args: Args, - context: Context -) => UMResolverResult; +export const acknowledgeSchema = t.type({ acknowledged: t.boolean }); + +export type AcknowledgeSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/response/index.ts b/x-pack/plugins/lists/common/schemas/response/index.ts new file mode 100644 index 0000000000000..3f11adf58d8d4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/index.ts @@ -0,0 +1,10 @@ +/* + * 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 * from './list_item_schema'; +export * from './list_schema'; +export * from './acknowledge_schema'; +export * from './list_item_index_exist_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.ts new file mode 100644 index 0000000000000..bf2bf21d2c216 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.ts @@ -0,0 +1,14 @@ +/* + * 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 t from 'io-ts'; + +export const listItemIndexExistSchema = t.type({ + list_index: t.boolean, + list_item_index: t.boolean, +}); + +export type ListItemIndexExistSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts new file mode 100644 index 0000000000000..6c2f2ed9a7095 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts @@ -0,0 +1,42 @@ +/* + * 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 t from 'io-ts'; + +/* eslint-disable @typescript-eslint/camelcase */ + +import { + created_at, + created_by, + id, + list_id, + metaOrUndefined, + tie_breaker_id, + type, + updated_at, + updated_by, + value, +} from '../common/schemas'; + +export const listItemSchema = t.exact( + t.type({ + created_at, + created_by, + id, + list_id, + meta: metaOrUndefined, + tie_breaker_id, + type, + updated_at, + updated_by, + value, + }) +); + +export type ListItemSchema = t.TypeOf; + +export const listItemArraySchema = t.array(listItemSchema); +export type ListItemArraySchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.ts new file mode 100644 index 0000000000000..cad449766ceb4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + created_at, + created_by, + description, + id, + metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, +} from '../common/schemas'; + +export const listSchema = t.exact( + t.type({ + created_at, + created_by, + description, + id, + meta: metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, + }) +); + +export type ListSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_string.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_string.ts new file mode 100644 index 0000000000000..d1e2094bbcad3 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_string.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 * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +export type NonEmptyStringC = t.Type; + +/** + * Types the NonEmptyString as: + * - A string that is not empty + */ +export const NonEmptyString: NonEmptyStringC = new t.Type( + 'NonEmptyString', + t.string.is, + (input, context): Either => { + if (typeof input === 'string' && input.trim() !== '') { + return t.success(input); + } else { + return t.failure(input, context); + } + }, + t.identity +); diff --git a/x-pack/plugins/lists/common/siem_common_deps.ts b/x-pack/plugins/lists/common/siem_common_deps.ts new file mode 100644 index 0000000000000..5e74753a6f0bd --- /dev/null +++ b/x-pack/plugins/lists/common/siem_common_deps.ts @@ -0,0 +1,8 @@ +/* + * 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 { getPaths, foldLeftRight } from '../../siem/server/utils/build_validation/__mocks__/utils'; +export { exactCheck } from '../../siem/server/utils/build_validation/exact_check'; diff --git a/x-pack/plugins/lists/kibana.json b/x-pack/plugins/lists/kibana.json new file mode 100644 index 0000000000000..b7aaac6d3fc76 --- /dev/null +++ b/x-pack/plugins/lists/kibana.json @@ -0,0 +1,10 @@ +{ + "configPath": ["xpack", "lists"], + "id": "lists", + "kibanaVersion": "kibana", + "requiredPlugins": [], + "optionalPlugins": ["spaces", "security"], + "server": true, + "ui": false, + "version": "8.0.0" +} diff --git a/x-pack/plugins/lists/server/config.ts b/x-pack/plugins/lists/server/config.ts new file mode 100644 index 0000000000000..3e7995b2ce8d0 --- /dev/null +++ b/x-pack/plugins/lists/server/config.ts @@ -0,0 +1,15 @@ +/* + * 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 { TypeOf, schema } from '@kbn/config-schema'; + +export const ConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), + listIndex: schema.string({ defaultValue: '.lists' }), + listItemIndex: schema.string({ defaultValue: '.items' }), +}); + +export type ConfigType = TypeOf; diff --git a/x-pack/plugins/lists/server/create_config.ts b/x-pack/plugins/lists/server/create_config.ts new file mode 100644 index 0000000000000..3158fabda935f --- /dev/null +++ b/x-pack/plugins/lists/server/create_config.ts @@ -0,0 +1,21 @@ +/* + * 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 { map } from 'rxjs/operators'; +import { PluginInitializerContext } from 'kibana/server'; +import { Observable } from 'rxjs'; + +import { ConfigType } from './config'; + +export const createConfig$ = ( + context: PluginInitializerContext +): Observable> => { + return context.config.create().pipe(map(config => config)); +}; diff --git a/x-pack/plugins/lists/server/error_with_status_code.ts b/x-pack/plugins/lists/server/error_with_status_code.ts new file mode 100644 index 0000000000000..f9bbbc4abad27 --- /dev/null +++ b/x-pack/plugins/lists/server/error_with_status_code.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. + */ + +export class ErrorWithStatusCode extends Error { + private readonly statusCode: number; + + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + } + + public getStatusCode = (): number => this.statusCode; +} diff --git a/x-pack/plugins/lists/server/get_space_id.test.ts b/x-pack/plugins/lists/server/get_space_id.test.ts new file mode 100644 index 0000000000000..9c1d11b71984d --- /dev/null +++ b/x-pack/plugins/lists/server/get_space_id.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { httpServerMock } from 'src/core/server/mocks'; +import { KibanaRequest } from 'src/core/server'; + +import { spacesServiceMock } from '../../spaces/server/spaces_service/spaces_service.mock'; + +import { getSpaceId } from './get_space_id'; + +describe('get_space_id', () => { + let request = KibanaRequest.from(httpServerMock.createRawRequest({})); + beforeEach(() => { + request = KibanaRequest.from(httpServerMock.createRawRequest({})); + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns "default" as the space id given a space id of "default"', () => { + const spaces = spacesServiceMock.createSetupContract(); + const space = getSpaceId({ request, spaces }); + expect(space).toEqual('default'); + }); + + test('it returns "another-space" as the space id given a space id of "another-space"', () => { + const spaces = spacesServiceMock.createSetupContract('another-space'); + const space = getSpaceId({ request, spaces }); + expect(space).toEqual('another-space'); + }); + + test('it returns "default" as the space id given a space id of undefined', () => { + const space = getSpaceId({ request, spaces: undefined }); + expect(space).toEqual('default'); + }); + + test('it returns "default" as the space id given a space id of null', () => { + const space = getSpaceId({ request, spaces: null }); + expect(space).toEqual('default'); + }); +}); diff --git a/x-pack/plugins/lists/server/get_space_id.ts b/x-pack/plugins/lists/server/get_space_id.ts new file mode 100644 index 0000000000000..f224e37e04467 --- /dev/null +++ b/x-pack/plugins/lists/server/get_space_id.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 { KibanaRequest } from 'kibana/server'; + +import { SpacesServiceSetup } from '../../spaces/server'; + +export const getSpaceId = ({ + spaces, + request, +}: { + spaces: SpacesServiceSetup | undefined | null; + request: KibanaRequest; +}): string => spaces?.getSpaceId(request) ?? 'default'; diff --git a/x-pack/plugins/lists/server/get_user.test.ts b/x-pack/plugins/lists/server/get_user.test.ts new file mode 100644 index 0000000000000..0992e3c361fcf --- /dev/null +++ b/x-pack/plugins/lists/server/get_user.test.ts @@ -0,0 +1,67 @@ +/* + * 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 { httpServerMock } from 'src/core/server/mocks'; +import { KibanaRequest } from 'src/core/server'; + +import { securityMock } from '../../security/server/mocks'; +import { SecurityPluginSetup } from '../../security/server'; + +import { getUser } from './get_user'; + +describe('get_user', () => { + let request = KibanaRequest.from(httpServerMock.createRawRequest({})); + beforeEach(() => { + jest.clearAllMocks(); + request = KibanaRequest.from(httpServerMock.createRawRequest({})); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns "bob" as the user given a security request with "bob"', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'bob' }); + const user = getUser({ request, security }); + expect(user).toEqual('bob'); + }); + + test('it returns "alice" as the user given a security request with "alice"', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'alice' }); + const user = getUser({ request, security }); + expect(user).toEqual('alice'); + }); + + test('it returns "elastic" as the user given null as the current user', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue(null); + const user = getUser({ request, security }); + expect(user).toEqual('elastic'); + }); + + test('it returns "elastic" as the user given undefined as the current user', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); + const user = getUser({ request, security }); + expect(user).toEqual('elastic'); + }); + + test('it returns "elastic" as the user given undefined as the plugin', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); + const user = getUser({ request, security: undefined }); + expect(user).toEqual('elastic'); + }); + + test('it returns "elastic" as the user given null as the plugin', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); + const user = getUser({ request, security: null }); + expect(user).toEqual('elastic'); + }); +}); diff --git a/x-pack/plugins/lists/server/get_user.ts b/x-pack/plugins/lists/server/get_user.ts new file mode 100644 index 0000000000000..3b59853d0ab62 --- /dev/null +++ b/x-pack/plugins/lists/server/get_user.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 { KibanaRequest } from 'kibana/server'; + +import { SecurityPluginSetup } from '../../security/server'; + +export interface GetUserOptions { + security: SecurityPluginSetup | null | undefined; + request: KibanaRequest; +} + +export const getUser = ({ security, request }: GetUserOptions): string => { + if (security != null) { + const authenticatedUser = security.authc.getCurrentUser(request); + if (authenticatedUser != null) { + return authenticatedUser.username; + } else { + return 'elastic'; + } + } else { + return 'elastic'; + } +}; diff --git a/x-pack/plugins/lists/server/index.ts b/x-pack/plugins/lists/server/index.ts new file mode 100644 index 0000000000000..c1e577aa60195 --- /dev/null +++ b/x-pack/plugins/lists/server/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { ConfigSchema } from './config'; +import { ListPlugin } from './plugin'; + +export const config = { schema: ConfigSchema }; +export const plugin = (initializerContext: PluginInitializerContext): ListPlugin => + new ListPlugin(initializerContext); diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts new file mode 100644 index 0000000000000..2498c36967a53 --- /dev/null +++ b/x-pack/plugins/lists/server/plugin.ts @@ -0,0 +1,86 @@ +/* + * 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 { first } from 'rxjs/operators'; +import { Logger, PluginInitializerContext } from 'kibana/server'; +import { CoreSetup } from 'src/core/server'; + +import { SecurityPluginSetup } from '../../security/server'; +import { SpacesServiceSetup } from '../../spaces/server'; + +import { ConfigType } from './config'; +import { initRoutes } from './routes/init_routes'; +import { ListClient } from './services/lists/client'; +import { ContextProvider, ContextProviderReturn, PluginsSetup } from './types'; +import { createConfig$ } from './create_config'; +import { getSpaceId } from './get_space_id'; +import { getUser } from './get_user'; + +export class ListPlugin { + private readonly logger: Logger; + private spaces: SpacesServiceSetup | undefined | null; + private config: ConfigType | undefined | null; + private security: SecurityPluginSetup | undefined | null; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.logger = this.initializerContext.logger.get(); + } + + public async setup(core: CoreSetup, plugins: PluginsSetup): Promise { + const config = await createConfig$(this.initializerContext) + .pipe(first()) + .toPromise(); + + this.logger.error( + 'You have activated the lists values feature flag which is NOT currently supported for Elastic Security! You should turn this feature flag off immediately by un-setting "xpack.lists.enabled: true" in kibana.yml and restarting Kibana' + ); + this.spaces = plugins.spaces?.spacesService; + this.config = config; + this.security = plugins.security; + + core.http.registerRouteHandlerContext('lists', this.createRouteHandlerContext()); + const router = core.http.createRouter(); + initRoutes(router); + } + + public start(): void { + this.logger.debug('Starting plugin'); + } + + public stop(): void { + this.logger.debug('Stopping plugin'); + } + + private createRouteHandlerContext = (): ContextProvider => { + return async (context, request): ContextProviderReturn => { + const { spaces, config, security } = this; + const { + core: { + elasticsearch: { + dataClient: { callAsCurrentUser }, + }, + }, + } = context; + if (config == null) { + throw new TypeError('Configuration is required for this plugin to operate'); + } else { + const spaceId = getSpaceId({ request, spaces }); + const user = getUser({ request, security }); + return { + getListClient: (): ListClient => + new ListClient({ + callCluster: callAsCurrentUser, + config, + request, + security, + spaceId, + user, + }), + }; + } + }; + }; +} diff --git a/x-pack/plugins/lists/server/routes/create_list_index_route.ts b/x-pack/plugins/lists/server/routes/create_list_index_route.ts new file mode 100644 index 0000000000000..1c893fb757c5d --- /dev/null +++ b/x-pack/plugins/lists/server/routes/create_list_index_route.ts @@ -0,0 +1,82 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { buildSiemResponse, transformError, validate } from '../siem_server_deps'; +import { LIST_INDEX } from '../../common/constants'; +import { acknowledgeSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const createListIndexRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: LIST_INDEX, + validate: false, + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const lists = getListClient(context); + const listIndexExists = await lists.getListIndexExists(); + const listItemIndexExists = await lists.getListItemIndexExists(); + + if (listIndexExists && listItemIndexExists) { + return siemResponse.error({ + body: `index: "${lists.getListIndex()}" and "${lists.getListItemIndex()}" already exists`, + statusCode: 409, + }); + } else { + const policyExists = await lists.getListPolicyExists(); + const policyListItemExists = await lists.getListItemPolicyExists(); + + if (!policyExists) { + await lists.setListPolicy(); + } + if (!policyListItemExists) { + await lists.setListItemPolicy(); + } + + const templateExists = await lists.getListTemplateExists(); + const templateListItemsExists = await lists.getListItemTemplateExists(); + + if (!templateExists) { + await lists.setListTemplate(); + } + + if (!templateListItemsExists) { + await lists.setListItemTemplate(); + } + + if (!listIndexExists) { + await lists.createListBootStrapIndex(); + } + if (!listItemIndexExists) { + await lists.createListItemBootStrapIndex(); + } + + const [validated, errors] = validate({ acknowledged: true }, acknowledgeSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/create_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_list_item_route.ts new file mode 100644 index 0000000000000..68622e98cbc52 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/create_list_item_route.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { createListItemSchema, listItemSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const createListItemRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: LIST_ITEM_URL, + validate: { + body: buildRouteValidation(createListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, value, meta } = request.body; + const lists = getListClient(context); + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${listId}" does not exist`, + statusCode: 404, + }); + } else { + const listItem = await lists.getListItemByValue({ listId, type: list.type, value }); + if (listItem.length !== 0) { + return siemResponse.error({ + body: `list_id: "${listId}" already contains the given value: ${value}`, + statusCode: 409, + }); + } else { + const createdListItem = await lists.createListItem({ + id, + listId, + meta, + type: list.type, + value, + }); + const [validated, errors] = validate(createdListItem, listItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/create_list_route.ts b/x-pack/plugins/lists/server/routes/create_list_route.ts new file mode 100644 index 0000000000000..0f3c404c53cfd --- /dev/null +++ b/x-pack/plugins/lists/server/routes/create_list_route.ts @@ -0,0 +1,69 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { createListSchema, listSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const createListRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: LIST_URL, + validate: { + body: buildRouteValidation(createListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { name, description, id, type, meta } = request.body; + const lists = getListClient(context); + const listExists = await lists.getListIndexExists(); + if (!listExists) { + return siemResponse.error({ + body: `To create a list, the index must exist first. Index "${lists.getListIndex()}" does not exist`, + statusCode: 400, + }); + } else { + if (id != null) { + const list = await lists.getList({ id }); + if (list != null) { + return siemResponse.error({ + body: `list id: "${id}" already exists`, + statusCode: 409, + }); + } + } + const list = await lists.createList({ description, id, meta, name, type }); + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts new file mode 100644 index 0000000000000..424c3f45aac40 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts @@ -0,0 +1,97 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_INDEX } from '../../common/constants'; +import { buildSiemResponse, transformError, validate } from '../siem_server_deps'; +import { acknowledgeSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +/** + * Deletes all of the indexes, template, ilm policies, and aliases. You can check + * this by looking at each of these settings from ES after a deletion: + * + * GET /_template/.lists-default + * GET /.lists-default-000001/ + * GET /_ilm/policy/.lists-default + * GET /_alias/.lists-default + * + * GET /_template/.items-default + * GET /.items-default-000001/ + * GET /_ilm/policy/.items-default + * GET /_alias/.items-default + * + * And ensuring they're all gone + */ +export const deleteListIndexRoute = (router: IRouter): void => { + router.delete( + { + options: { + tags: ['access:lists'], + }, + path: LIST_INDEX, + validate: false, + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const lists = getListClient(context); + const listIndexExists = await lists.getListIndexExists(); + const listItemIndexExists = await lists.getListItemIndexExists(); + + if (!listIndexExists && !listItemIndexExists) { + return siemResponse.error({ + body: `index: "${lists.getListIndex()}" and "${lists.getListItemIndex()}" does not exist`, + statusCode: 404, + }); + } else { + if (listIndexExists) { + await lists.deleteListIndex(); + } + if (listItemIndexExists) { + await lists.deleteListItemIndex(); + } + + const listsPolicyExists = await lists.getListPolicyExists(); + const listItemPolicyExists = await lists.getListItemPolicyExists(); + + if (listsPolicyExists) { + await lists.deleteListPolicy(); + } + if (listItemPolicyExists) { + await lists.deleteListItemPolicy(); + } + + const listsTemplateExists = await lists.getListTemplateExists(); + const listItemTemplateExists = await lists.getListItemTemplateExists(); + + if (listsTemplateExists) { + await lists.deleteListTemplate(); + } + if (listItemTemplateExists) { + await lists.deleteListItemTemplate(); + } + + const [validated, errors] = validate({ acknowledged: true }, acknowledgeSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts new file mode 100644 index 0000000000000..51b4eb9f02cc2 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts @@ -0,0 +1,89 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { deleteListItemSchema, listItemArraySchema, listItemSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const deleteListItemRoute = (router: IRouter): void => { + router.delete( + { + options: { + tags: ['access:lists'], + }, + path: LIST_ITEM_URL, + validate: { + query: buildRouteValidation(deleteListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, value } = request.query; + const lists = getListClient(context); + if (id != null) { + const deleted = await lists.deleteListItem({ id }); + if (deleted == null) { + return siemResponse.error({ + body: `list item with id: "${id}" item not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(deleted, listItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } else if (listId != null && value != null) { + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list_id: "${listId}" does not exist`, + statusCode: 404, + }); + } else { + const deleted = await lists.deleteListItemByValue({ listId, type: list.type, value }); + if (deleted == null || deleted.length === 0) { + return siemResponse.error({ + body: `list_id: "${listId}" with ${value} was not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(deleted, listItemArraySchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } + } else { + return siemResponse.error({ + body: `Either "list_id" or "id" needs to be defined in the request`, + statusCode: 400, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/delete_list_route.ts b/x-pack/plugins/lists/server/routes/delete_list_route.ts new file mode 100644 index 0000000000000..e89355b7689c5 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/delete_list_route.ts @@ -0,0 +1,59 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { deleteListSchema, listSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const deleteListRoute = (router: IRouter): void => { + router.delete( + { + options: { + tags: ['access:lists'], + }, + path: LIST_URL, + validate: { + query: buildRouteValidation(deleteListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const lists = getListClient(context); + const { id } = request.query; + const deleted = await lists.deleteList({ id }); + if (deleted == null) { + return siemResponse.error({ + body: `list id: "${id}" was not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(deleted, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/export_list_item_route.ts b/x-pack/plugins/lists/server/routes/export_list_item_route.ts new file mode 100644 index 0000000000000..32b99bfc512bf --- /dev/null +++ b/x-pack/plugins/lists/server/routes/export_list_item_route.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 { Stream } from 'stream'; + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { exportListItemQuerySchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const exportListItemRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: `${LIST_ITEM_URL}/_export`, + validate: { + query: buildRouteValidation(exportListItemQuerySchema), + // TODO: Do we want to add a body here like export_rules_route and allow a size limit? + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { list_id: listId } = request.query; + const lists = getListClient(context); + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list_id: ${listId} does not exist`, + statusCode: 400, + }); + } else { + // TODO: Allow the API to override the name of the file to export + const fileName = list.name; + + const stream = new Stream.PassThrough(); + lists.exportListItemsToStream({ listId, stream, stringToAppend: '\n' }); + return response.ok({ + body: stream, + headers: { + 'Content-Disposition': `attachment; filename="${fileName}"`, + 'Content-Type': 'text/plain', + }, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts new file mode 100644 index 0000000000000..a3b6a520a4ecf --- /dev/null +++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts @@ -0,0 +1,105 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { + ImportListItemSchema, + importListItemQuerySchema, + importListItemSchema, + listSchema, +} from '../../common/schemas'; + +import { getListClient } from '.'; + +export const importListItemRoute = (router: IRouter): void => { + router.post( + { + options: { + body: { + output: 'stream', + }, + tags: ['access:lists'], + }, + path: `${LIST_ITEM_URL}/_import`, + validate: { + body: buildRouteValidation( + importListItemSchema + ), + query: buildRouteValidation(importListItemQuerySchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { list_id: listId, type } = request.query; + const lists = getListClient(context); + if (listId != null) { + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${listId}" does not exist`, + statusCode: 409, + }); + } + await lists.importListItemsToStream({ + listId, + meta: undefined, + stream: request.body.file, + type: list.type, + }); + + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } else if (type != null) { + const { filename } = request.body.file.hapi; + // TODO: Should we prevent the same file from being uploaded multiple times? + const list = await lists.createListIfItDoesNotExist({ + description: `File uploaded from file system of ${filename}`, + id: filename, + meta: undefined, + name: filename, + type, + }); + await lists.importListItemsToStream({ + listId: list.id, + meta: undefined, + stream: request.body.file, + type: list.type, + }); + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } else { + return siemResponse.error({ + body: 'Either type or list_id need to be defined in the query', + statusCode: 400, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/index.ts b/x-pack/plugins/lists/server/routes/index.ts new file mode 100644 index 0000000000000..4951cddc56939 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/index.ts @@ -0,0 +1,21 @@ +/* + * 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 * from './create_list_index_route'; +export * from './create_list_item_route'; +export * from './create_list_route'; +export * from './delete_list_index_route'; +export * from './delete_list_item_route'; +export * from './delete_list_route'; +export * from './export_list_item_route'; +export * from './import_list_item_route'; +export * from './init_routes'; +export * from './patch_list_item_route'; +export * from './patch_list_route'; +export * from './read_list_index_route'; +export * from './read_list_item_route'; +export * from './read_list_route'; +export * from './utils'; diff --git a/x-pack/plugins/lists/server/routes/init_routes.ts b/x-pack/plugins/lists/server/routes/init_routes.ts new file mode 100644 index 0000000000000..924dd086ee708 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/init_routes.ts @@ -0,0 +1,49 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { updateListRoute } from './update_list_route'; +import { updateListItemRoute } from './update_list_item_route'; + +import { + createListIndexRoute, + createListItemRoute, + createListRoute, + deleteListIndexRoute, + deleteListItemRoute, + deleteListRoute, + exportListItemRoute, + importListItemRoute, + patchListItemRoute, + patchListRoute, + readListIndexRoute, + readListItemRoute, + readListRoute, +} from '.'; + +export const initRoutes = (router: IRouter): void => { + // lists + createListRoute(router); + readListRoute(router); + updateListRoute(router); + deleteListRoute(router); + patchListRoute(router); + + // lists items + createListItemRoute(router); + readListItemRoute(router); + updateListItemRoute(router); + deleteListItemRoute(router); + patchListItemRoute(router); + exportListItemRoute(router); + importListItemRoute(router); + + // indexes of lists + createListIndexRoute(router); + readListIndexRoute(router); + deleteListIndexRoute(router); +}; diff --git a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts new file mode 100644 index 0000000000000..e18fd0618b133 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/patch_list_item_route.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 { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listItemSchema, patchListItemSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const patchListItemRoute = (router: IRouter): void => { + router.patch( + { + options: { + tags: ['access:lists'], + }, + path: LIST_ITEM_URL, + validate: { + body: buildRouteValidation(patchListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { value, id, meta } = request.body; + const lists = getListClient(context); + const listItem = await lists.updateListItem({ + id, + meta, + value, + }); + if (listItem == null) { + return siemResponse.error({ + body: `list item id: "${id}" not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(listItem, listItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/patch_list_route.ts b/x-pack/plugins/lists/server/routes/patch_list_route.ts new file mode 100644 index 0000000000000..9d3fa4db8ccd0 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/patch_list_route.ts @@ -0,0 +1,59 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listSchema, patchListSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const patchListRoute = (router: IRouter): void => { + router.patch( + { + options: { + tags: ['access:lists'], + }, + path: LIST_URL, + validate: { + body: buildRouteValidation(patchListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { name, description, id, meta } = request.body; + const lists = getListClient(context); + const list = await lists.updateList({ description, id, meta, name }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${id}" found found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/read_list_index_route.ts new file mode 100644 index 0000000000000..248fc72666d70 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/read_list_index_route.ts @@ -0,0 +1,67 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_INDEX } from '../../common/constants'; +import { buildSiemResponse, transformError, validate } from '../siem_server_deps'; +import { listItemIndexExistSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const readListIndexRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists'], + }, + path: LIST_INDEX, + validate: false, + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const lists = getListClient(context); + const listIndexExists = await lists.getListIndexExists(); + const listItemIndexExists = await lists.getListItemIndexExists(); + + if (listIndexExists || listItemIndexExists) { + const [validated, errors] = validate( + { list_index: listIndexExists, lists_item_index: listItemIndexExists }, + listItemIndexExistSchema + ); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } else if (!listIndexExists && listItemIndexExists) { + return siemResponse.error({ + body: `index ${lists.getListIndex()} does not exist`, + statusCode: 404, + }); + } else if (!listItemIndexExists && listIndexExists) { + return siemResponse.error({ + body: `index ${lists.getListItemIndex()} does not exist`, + statusCode: 404, + }); + } else { + return siemResponse.error({ + body: `index ${lists.getListIndex()} and index ${lists.getListItemIndex()} does not exist`, + statusCode: 404, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/read_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_list_item_route.ts new file mode 100644 index 0000000000000..0a60cba786f04 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/read_list_item_route.ts @@ -0,0 +1,93 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listItemArraySchema, listItemSchema, readListItemSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const readListItemRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists'], + }, + path: LIST_ITEM_URL, + validate: { + query: buildRouteValidation(readListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, value } = request.query; + const lists = getListClient(context); + if (id != null) { + const listItem = await lists.getListItem({ id }); + if (listItem == null) { + return siemResponse.error({ + body: `list item id: "${id}" does not exist`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(listItem, listItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } else if (listId != null && value != null) { + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${listId}" does not exist`, + statusCode: 404, + }); + } else { + const listItem = await lists.getListItemByValue({ + listId, + type: list.type, + value, + }); + if (listItem.length === 0) { + return siemResponse.error({ + body: `list_id: "${listId}" item of ${value} does not exist`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(listItem, listItemArraySchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } + } else { + return siemResponse.error({ + body: `Either "list_id" or "id" needs to be defined in the request`, + statusCode: 400, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/read_list_route.ts b/x-pack/plugins/lists/server/routes/read_list_route.ts new file mode 100644 index 0000000000000..c30eadfca0b65 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/read_list_route.ts @@ -0,0 +1,59 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listSchema, readListSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const readListRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists'], + }, + path: LIST_URL, + validate: { + query: buildRouteValidation(readListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id } = request.query; + const lists = getListClient(context); + const list = await lists.getList({ id }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${id}" does not exist`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/update_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_list_item_route.ts new file mode 100644 index 0000000000000..494d57b93b8e4 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/update_list_item_route.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 { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listItemSchema, updateListItemSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const updateListItemRoute = (router: IRouter): void => { + router.put( + { + options: { + tags: ['access:lists'], + }, + path: LIST_ITEM_URL, + validate: { + body: buildRouteValidation(updateListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { value, id, meta } = request.body; + const lists = getListClient(context); + const listItem = await lists.updateListItem({ + id, + meta, + value, + }); + if (listItem == null) { + return siemResponse.error({ + body: `list item id: "${id}" not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(listItem, listItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/update_list_route.ts b/x-pack/plugins/lists/server/routes/update_list_route.ts new file mode 100644 index 0000000000000..6ace61e46a780 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/update_list_route.ts @@ -0,0 +1,59 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listSchema, updateListSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const updateListRoute = (router: IRouter): void => { + router.put( + { + options: { + tags: ['access:lists'], + }, + path: LIST_URL, + validate: { + body: buildRouteValidation(updateListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { name, description, id, meta } = request.body; + const lists = getListClient(context); + const list = await lists.updateList({ description, id, meta, name }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${id}" found found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/utils/get_list_client.ts b/x-pack/plugins/lists/server/routes/utils/get_list_client.ts new file mode 100644 index 0000000000000..a16163ec0fa3a --- /dev/null +++ b/x-pack/plugins/lists/server/routes/utils/get_list_client.ts @@ -0,0 +1,19 @@ +/* + * 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 { RequestHandlerContext } from 'kibana/server'; + +import { ListClient } from '../../services/lists/client'; +import { ErrorWithStatusCode } from '../../error_with_status_code'; + +export const getListClient = (context: RequestHandlerContext): ListClient => { + const lists = context.lists?.getListClient(); + if (lists == null) { + throw new ErrorWithStatusCode('Lists is not found as a plugin', 404); + } else { + return lists; + } +}; diff --git a/x-pack/legacy/plugins/alerting/index.ts b/x-pack/plugins/lists/server/routes/utils/index.ts similarity index 87% rename from x-pack/legacy/plugins/alerting/index.ts rename to x-pack/plugins/lists/server/routes/utils/index.ts index 0d0a698841269..a601bdfc003c5 100644 --- a/x-pack/legacy/plugins/alerting/index.ts +++ b/x-pack/plugins/lists/server/routes/utils/index.ts @@ -3,5 +3,4 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -export * from './server'; +export * from './get_list_client'; diff --git a/x-pack/plugins/lists/server/scripts/check_env_variables.sh b/x-pack/plugins/lists/server/scripts/check_env_variables.sh new file mode 100755 index 0000000000000..fb3bbbe0fad18 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/check_env_variables.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +# +# 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. +# + +# Add this to the start of any scripts to detect if env variables are set + +set -e + +if [ -z "${ELASTICSEARCH_USERNAME}" ]; then + echo "Set ELASTICSEARCH_USERNAME in your environment" + exit 1 +fi + +if [ -z "${ELASTICSEARCH_PASSWORD}" ]; then + echo "Set ELASTICSEARCH_PASSWORD in your environment" + exit 1 +fi + +if [ -z "${ELASTICSEARCH_URL}" ]; then + echo "Set ELASTICSEARCH_URL in your environment" + exit 1 +fi + +if [ -z "${KIBANA_URL}" ]; then + echo "Set KIBANA_URL in your environment" + exit 1 +fi + +if [ -z "${TASK_MANAGER_INDEX}" ]; then + echo "Set TASK_MANAGER_INDEX in your environment" + exit 1 +fi + +if [ -z "${KIBANA_INDEX}" ]; then + echo "Set KIBANA_INDEX in your environment" + exit 1 +fi diff --git a/x-pack/plugins/lists/server/scripts/delete_all_lists.sh b/x-pack/plugins/lists/server/scripts/delete_all_lists.sh new file mode 100755 index 0000000000000..5b65bb14414c7 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_all_lists.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_all_lists.sh +# https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html + + +# Delete all the main lists that have children items +curl -s -k \ + -H "Content-Type: application/json" \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${ELASTICSEARCH_URL}/${KIBANA_INDEX}*/_delete_by_query \ + --data '{ + "query": { + "exists": { "field": "siem_list" } + } + }' \ + | jq . + +# Delete all the list children items as well +curl -s -k \ + -H "Content-Type: application/json" \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${ELASTICSEARCH_URL}/${KIBANA_INDEX}*/_delete_by_query \ + --data '{ + "query": { + "exists": { "field": "siem_list_item" } + } + }' \ + | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_list.sh b/x-pack/plugins/lists/server/scripts/delete_list.sh new file mode 100755 index 0000000000000..9934ce61c7107 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_list.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_list_by_list_id.sh ${list_id} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/lists?id="$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_list_index.sh b/x-pack/plugins/lists/server/scripts/delete_list_index.sh new file mode 100755 index 0000000000000..85f06ffbd6670 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_list_index.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_signal_index.sh +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/lists/index | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_list_item_by_id.sh b/x-pack/plugins/lists/server/scripts/delete_list_item_by_id.sh new file mode 100755 index 0000000000000..ab14d8c8a80ed --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_list_item_by_id.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_list_item_by_id.sh?id={id} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/lists/items?id=$1 | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_list_item_by_value.sh b/x-pack/plugins/lists/server/scripts/delete_list_item_by_value.sh new file mode 100755 index 0000000000000..6d3213ccb8793 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_list_item_by_value.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_list_item_by_value.sh?list_id=${some_id}&value=${some_ip} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE "${KIBANA_URL}${SPACE_URL}/api/lists/items?list_id=$1&value=$2" | jq . diff --git a/x-pack/plugins/lists/server/scripts/export_list_items.sh b/x-pack/plugins/lists/server/scripts/export_list_items.sh new file mode 100755 index 0000000000000..ba355854c77cc --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/export_list_items.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a defaults if no argument is specified +LIST_ID=${1:-ips.txt} + +# Example to export +# ./export_list_items.sh > /tmp/ips.txt + +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/lists/items/_export?list_id=${LIST_ID}" diff --git a/x-pack/plugins/lists/server/scripts/export_list_items_to_file.sh b/x-pack/plugins/lists/server/scripts/export_list_items_to_file.sh new file mode 100755 index 0000000000000..5efad01e9a68e --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/export_list_items_to_file.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a defaults if no argument is specified +FOLDER=${1:-/tmp} + +# Example to export +# ./export_list_items_to_file.sh + +# Change current working directory as exports cause Kibana to restart +pushd ${FOLDER} > /dev/null + +curl -s -k -OJ \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/lists/items/_export?list_id=list-ip" + +popd > /dev/null diff --git a/x-pack/plugins/lists/server/scripts/get_list.sh b/x-pack/plugins/lists/server/scripts/get_list.sh new file mode 100755 index 0000000000000..7f0e4e3062266 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/get_list.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./get_list.sh {list_id} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/lists?id="$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/get_list_item_by_id.sh b/x-pack/plugins/lists/server/scripts/get_list_item_by_id.sh new file mode 100755 index 0000000000000..31d26e195815f --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/get_list_item_by_id.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./get_list_item_by_id ${id} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items?id=$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/get_list_item_by_value.sh b/x-pack/plugins/lists/server/scripts/get_list_item_by_value.sh new file mode 100755 index 0000000000000..24ca27b0c949d --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/get_list_item_by_value.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./get_list_item_by_value.sh ${list_id} ${value} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items?list_id=$1&value=$2" | jq . diff --git a/x-pack/plugins/lists/server/scripts/hard_reset.sh b/x-pack/plugins/lists/server/scripts/hard_reset.sh new file mode 100755 index 0000000000000..861928866369b --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/hard_reset.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# re-create the list and list item indexes +./delete_list_index.sh +./post_list_index.sh diff --git a/x-pack/plugins/lists/server/scripts/import_list_items.sh b/x-pack/plugins/lists/server/scripts/import_list_items.sh new file mode 100755 index 0000000000000..a39409cd08267 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/import_list_items.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a defaults if no argument is specified +LIST_ID=${1:-list-ip} +FILE=${2:-./lists/files/ips.txt} + +# ./import_list_items.sh list-ip ./lists/files/ips.txt +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/lists/items/_import?list_id=${LIST_ID}" \ + --form file=@${FILE} \ + | jq .; diff --git a/x-pack/plugins/lists/server/scripts/import_list_items_by_filename.sh b/x-pack/plugins/lists/server/scripts/import_list_items_by_filename.sh new file mode 100755 index 0000000000000..4ec55cb4c5f7b --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/import_list_items_by_filename.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a defaults if no argument is specified +TYPE=${1:-ip} +FILE=${2:-./lists/files/ips.txt} + +# Example to import ips from ./lists/files/ips.txt +# ./import_list_items_by_filename.sh ip ./lists/files/ips.txt + +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/lists/items/_import?type=${TYPE}" \ + --form file=@${FILE} \ + | jq .; diff --git a/x-pack/plugins/lists/server/scripts/lists/files/hosts.txt b/x-pack/plugins/lists/server/scripts/lists/files/hosts.txt new file mode 100644 index 0000000000000..aee32e3a4bd92 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/files/hosts.txt @@ -0,0 +1,2 @@ +kibana +rock01 diff --git a/x-pack/plugins/lists/server/scripts/lists/files/ips.txt b/x-pack/plugins/lists/server/scripts/lists/files/ips.txt new file mode 100644 index 0000000000000..cf8ebcacae5a1 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/files/ips.txt @@ -0,0 +1,9 @@ +127.0.0.1 +127.0.0.2 +127.0.0.3 +127.0.0.4 +127.0.0.5 +127.0.0.6 +127.0.0.7 +127.0.0.8 +127.0.0.9 diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_everything.json b/x-pack/plugins/lists/server/scripts/lists/new/list_everything.json new file mode 100644 index 0000000000000..196b3b149ab82 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_everything.json @@ -0,0 +1,13 @@ +{ + "id": "list-ip-everything", + "name": "Simple list with an ip", + "description": "This list describes bad internet ip", + "type": "ip", + "meta": { + "level_1_meta": { + "level_2_meta": { + "level_3_key": "some_value_ui" + } + } + } +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_ip.json b/x-pack/plugins/lists/server/scripts/lists/new/list_ip.json new file mode 100644 index 0000000000000..3e12ef1754f07 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_ip.json @@ -0,0 +1,6 @@ +{ + "id": "list-ip", + "name": "Simple list with an ip", + "description": "This list describes bad internet ip", + "type": "ip" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json new file mode 100644 index 0000000000000..1516fa5057e50 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json @@ -0,0 +1,5 @@ +{ + "id": "hand_inserted_item_id", + "list_id": "list-ip", + "value": "127.0.0.1" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item_everything.json b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item_everything.json new file mode 100644 index 0000000000000..9730c1b7523f1 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item_everything.json @@ -0,0 +1,12 @@ +{ + "id": "hand_inserted_item_id_everything", + "list_id": "list-ip", + "value": "127.0.0.2", + "meta": { + "level_1_meta": { + "level_2_meta": { + "level_3_key": "some_value_ui" + } + } + } +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_ip_no_id.json b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_no_id.json new file mode 100644 index 0000000000000..4a95a62b67c3e --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_no_id.json @@ -0,0 +1,5 @@ +{ + "name": "Simple list with an ip", + "description": "This list describes bad internet ip", + "type": "ip" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_keyword.json b/x-pack/plugins/lists/server/scripts/lists/new/list_keyword.json new file mode 100644 index 0000000000000..e8f5fa7e38a06 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_keyword.json @@ -0,0 +1,6 @@ +{ + "id": "list-keyword", + "name": "Simple list with a keyword", + "description": "This list describes bad host names", + "type": "keyword" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_keyword_item.json b/x-pack/plugins/lists/server/scripts/lists/new/list_keyword_item.json new file mode 100644 index 0000000000000..b736e7b96ad98 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_keyword_item.json @@ -0,0 +1,4 @@ +{ + "list_id": "list-keyword", + "value": "kibana" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/patches/list_ip_item.json b/x-pack/plugins/lists/server/scripts/lists/patches/list_ip_item.json new file mode 100644 index 0000000000000..00c3496e71b35 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/patches/list_ip_item.json @@ -0,0 +1,4 @@ +{ + "id": "hand_inserted_item_id", + "value": "255.255.255.255" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/patches/simplest_updated_name.json b/x-pack/plugins/lists/server/scripts/lists/patches/simplest_updated_name.json new file mode 100644 index 0000000000000..1a57ab8b6a3b9 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/patches/simplest_updated_name.json @@ -0,0 +1,4 @@ +{ + "id": "list-ip", + "name": "Changed the name here to something else" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/updates/list_ip_item.json b/x-pack/plugins/lists/server/scripts/lists/updates/list_ip_item.json new file mode 100644 index 0000000000000..00c3496e71b35 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/updates/list_ip_item.json @@ -0,0 +1,4 @@ +{ + "id": "hand_inserted_item_id", + "value": "255.255.255.255" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/updates/simple_update.json b/x-pack/plugins/lists/server/scripts/lists/updates/simple_update.json new file mode 100644 index 0000000000000..936a070ede52c --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/updates/simple_update.json @@ -0,0 +1,5 @@ +{ + "id": "list-ip", + "name": "Changed the name here to something else", + "description": "Some other description here for you" +} diff --git a/x-pack/plugins/lists/server/scripts/lists_index_exists.sh b/x-pack/plugins/lists/server/scripts/lists_index_exists.sh new file mode 100755 index 0000000000000..7dfbd5b1bada5 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists_index_exists.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./lists_index_exists.sh +curl -s -k -f \ + -H 'Content-Type: application/json' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${KIBANA_URL}${SPACE_URL}/api/lists/index | jq . diff --git a/x-pack/plugins/lists/server/scripts/patch_list.sh b/x-pack/plugins/lists/server/scripts/patch_list.sh new file mode 100755 index 0000000000000..3a517a52dbd21 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/patch_list.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/patches/simplest_updated_name.json}) + +# Example: ./patch_list.sh +# Example: ./patch_list.sh ./lists/patches/simplest_updated_name.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PATCH ${KIBANA_URL}${SPACE_URL}/api/lists \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/patch_list_item.sh b/x-pack/plugins/lists/server/scripts/patch_list_item.sh new file mode 100755 index 0000000000000..406b03dc6499c --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/patch_list_item.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/patches/list_ip_item.json}) + +# Example: ./patch_list.sh +# Example: ./patch_list.sh ./lists/patches/list_ip_item.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PATCH ${KIBANA_URL}${SPACE_URL}/api/lists/items \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/post_list.sh b/x-pack/plugins/lists/server/scripts/post_list.sh new file mode 100755 index 0000000000000..6aaffee0bc4b2 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/post_list.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/new/list_ip.json}) + +# Example: ./post_list.sh +# Example: ./post_list.sh ./lists/new/list_ip.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/lists \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/post_list_index.sh b/x-pack/plugins/lists/server/scripts/post_list_index.sh new file mode 100755 index 0000000000000..b7c372d3947e3 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/post_list_index.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./post_signal_index.sh +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/lists/index | jq . diff --git a/x-pack/plugins/lists/server/scripts/post_list_item.sh b/x-pack/plugins/lists/server/scripts/post_list_item.sh new file mode 100755 index 0000000000000..b55a60420674f --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/post_list_item.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/new/list_ip_item.json}) + +# Example: ./post_list.sh +# Example: ./post_list.sh ./lists/new/list_ip_item.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/lists/items \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/update_list.sh b/x-pack/plugins/lists/server/scripts/update_list.sh new file mode 100755 index 0000000000000..4d93544d568a8 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/update_list.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/updates/simple_update.json}) + +# Example: ./update_list.sh +# Example: ./update_list.sh ./lists/updates/simple_update.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PUT ${KIBANA_URL}${SPACE_URL}/api/lists \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/update_list_item.sh b/x-pack/plugins/lists/server/scripts/update_list_item.sh new file mode 100755 index 0000000000000..e3153bfd25b19 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/update_list_item.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/updates/list_ip_item.json}) + +# Example: ./patch_list.sh +# Example: ./patch_list.sh ./lists/updates/list_ip_item.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PATCH ${KIBANA_URL}${SPACE_URL}/api/lists/items \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts new file mode 100644 index 0000000000000..48deb3ee86820 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts @@ -0,0 +1,88 @@ +/* + * 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 { TestReadable } from '../mocks'; + +import { BufferLines } from './buffer_lines'; + +describe('buffer_lines', () => { + test('it can read a single line', done => { + const input = new TestReadable(); + input.push('line one\n'); + input.push(null); + const bufferedLine = new BufferLines({ input }); + let linesToTest: string[] = []; + bufferedLine.on('lines', (lines: string[]) => { + linesToTest = [...linesToTest, ...lines]; + }); + bufferedLine.on('close', () => { + expect(linesToTest).toEqual(['line one']); + done(); + }); + }); + + test('it can read two lines', done => { + const input = new TestReadable(); + input.push('line one\n'); + input.push('line two\n'); + input.push(null); + const bufferedLine = new BufferLines({ input }); + let linesToTest: string[] = []; + bufferedLine.on('lines', (lines: string[]) => { + linesToTest = [...linesToTest, ...lines]; + }); + bufferedLine.on('close', () => { + expect(linesToTest).toEqual(['line one', 'line two']); + done(); + }); + }); + + test('two identical lines are collapsed into just one line without duplicates', done => { + const input = new TestReadable(); + input.push('line one\n'); + input.push('line one\n'); + input.push(null); + const bufferedLine = new BufferLines({ input }); + let linesToTest: string[] = []; + bufferedLine.on('lines', (lines: string[]) => { + linesToTest = [...linesToTest, ...lines]; + }); + bufferedLine.on('close', () => { + expect(linesToTest).toEqual(['line one']); + done(); + }); + }); + + test('it can close out without writing any lines', done => { + const input = new TestReadable(); + input.push(null); + const bufferedLine = new BufferLines({ input }); + let linesToTest: string[] = []; + bufferedLine.on('lines', (lines: string[]) => { + linesToTest = [...linesToTest, ...lines]; + }); + bufferedLine.on('close', () => { + expect(linesToTest).toEqual([]); + done(); + }); + }); + + test('it can read 200 lines', done => { + const input = new TestReadable(); + const bufferedLine = new BufferLines({ input }); + let linesToTest: string[] = []; + const size200: string[] = new Array(200).fill(null).map((_, index) => `${index}\n`); + size200.forEach(element => input.push(element)); + input.push(null); + bufferedLine.on('lines', (lines: string[]) => { + linesToTest = [...linesToTest, ...lines]; + }); + bufferedLine.on('close', () => { + expect(linesToTest.length).toEqual(200); + done(); + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/buffer_lines.ts b/x-pack/plugins/lists/server/services/items/buffer_lines.ts new file mode 100644 index 0000000000000..fd8fe7077fd58 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/buffer_lines.ts @@ -0,0 +1,50 @@ +/* + * 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 readLine from 'readline'; +import { Readable } from 'stream'; + +const BUFFER_SIZE = 100; + +export class BufferLines extends Readable { + private set = new Set(); + constructor({ input }: { input: NodeJS.ReadableStream }) { + super({ encoding: 'utf-8' }); + const readline = readLine.createInterface({ + input, + }); + + readline.on('line', line => { + this.push(line); + }); + + readline.on('close', () => { + this.push(null); + }); + } + + public _read(): void { + // No operation but this is required to be implemented + } + + public push(line: string | null): boolean { + if (line == null) { + this.emit('lines', Array.from(this.set)); + this.set.clear(); + this.emit('close'); + return true; + } else { + this.set.add(line); + if (this.set.size > BUFFER_SIZE) { + this.emit('lines', Array.from(this.set)); + this.set.clear(); + return true; + } else { + return true; + } + } + } +} diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts new file mode 100644 index 0000000000000..abbb270149955 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { + LIST_ITEM_ID, + LIST_ITEM_INDEX, + getCreateListItemOptionsMock, + getIndexESListItemMock, + getListItemResponseMock, +} from '../mocks'; + +import { createListItem } from './create_list_item'; + +describe('crete_list_item', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list item as expected with the id changed out for the elastic id', async () => { + const options = getCreateListItemOptionsMock(); + const listItem = await createListItem(options); + const expected = getListItemResponseMock(); + expected.id = 'elastic-id-123'; + expect(listItem).toEqual(expected); + }); + + test('It calls "callCluster" with body, index, and listIndex', async () => { + const options = getCreateListItemOptionsMock(); + await createListItem(options); + const body = getIndexESListItemMock(); + const expected = { + body, + id: LIST_ITEM_ID, + index: LIST_ITEM_INDEX, + }; + expect(options.callCluster).toBeCalledWith('index', expected); + }); + + test('It returns an auto-generated id if id is sent in undefined', async () => { + const options = getCreateListItemOptionsMock(); + options.id = undefined; + const list = await createListItem(options); + const expected = getListItemResponseMock(); + expected.id = 'elastic-id-123'; + expect(list).toEqual(expected); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts new file mode 100644 index 0000000000000..83a118b795192 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -0,0 +1,73 @@ +/* + * 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 uuid from 'uuid'; +import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { + IdOrUndefined, + IndexEsListItemSchema, + ListItemSchema, + MetaOrUndefined, + Type, +} from '../../../common/schemas'; +import { transformListItemToElasticQuery } from '../utils'; + +export interface CreateListItemOptions { + id: IdOrUndefined; + listId: string; + type: Type; + value: string; + callCluster: APICaller; + listItemIndex: string; + user: string; + meta: MetaOrUndefined; + dateNow?: string; + tieBreaker?: string; +} + +export const createListItem = async ({ + id, + listId, + type, + value, + callCluster, + listItemIndex, + user, + meta, + dateNow, + tieBreaker, +}: CreateListItemOptions): Promise => { + const createdAt = dateNow ?? new Date().toISOString(); + const tieBreakerId = tieBreaker ?? uuid.v4(); + const baseBody = { + created_at: createdAt, + created_by: user, + list_id: listId, + meta, + tie_breaker_id: tieBreakerId, + updated_at: createdAt, + updated_by: user, + }; + const body: IndexEsListItemSchema = { + ...baseBody, + ...transformListItemToElasticQuery({ type, value }), + }; + + const response: CreateDocumentResponse = await callCluster('index', { + body, + id, + index: listItemIndex, + }); + + return { + id: response._id, + type, + value, + ...baseBody, + }; +}; diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts new file mode 100644 index 0000000000000..94cc57b53b4e2 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts @@ -0,0 +1,49 @@ +/* + * 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 { IndexEsListItemSchema } from '../../../common/schemas'; +import { + LIST_ITEM_INDEX, + TIE_BREAKERS, + VALUE_2, + getCreateListItemBulkOptionsMock, + getIndexESListItemMock, +} from '../mocks'; + +import { createListItemsBulk } from './create_list_items_bulk'; + +describe('crete_list_item_bulk', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('It calls "callCluster" with body, index, and the bulk items', async () => { + const options = getCreateListItemBulkOptionsMock(); + await createListItemsBulk(options); + const firstRecord: IndexEsListItemSchema = getIndexESListItemMock(); + const secondRecord: IndexEsListItemSchema = getIndexESListItemMock(VALUE_2); + [firstRecord.tie_breaker_id, secondRecord.tie_breaker_id] = TIE_BREAKERS; + expect(options.callCluster).toBeCalledWith('bulk', { + body: [ + { create: { _index: LIST_ITEM_INDEX } }, + firstRecord, + { create: { _index: LIST_ITEM_INDEX } }, + secondRecord, + ], + index: LIST_ITEM_INDEX, + }); + }); + + test('It should not call the dataClient when the values are empty', async () => { + const options = getCreateListItemBulkOptionsMock(); + options.value = []; + expect(options.callCluster).not.toBeCalled(); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts new file mode 100644 index 0000000000000..eac294c5f244a --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts @@ -0,0 +1,70 @@ +/* + * 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 uuid from 'uuid'; +import { APICaller } from 'kibana/server'; + +import { transformListItemToElasticQuery } from '../utils'; +import { + CreateEsBulkTypeSchema, + IndexEsListItemSchema, + MetaOrUndefined, + Type, +} from '../../../common/schemas'; + +export interface CreateListItemsBulkOptions { + listId: string; + type: Type; + value: string[]; + callCluster: APICaller; + listItemIndex: string; + user: string; + meta: MetaOrUndefined; + dateNow?: string; + tieBreaker?: string[]; +} + +export const createListItemsBulk = async ({ + listId, + type, + value, + callCluster, + listItemIndex, + user, + meta, + dateNow, + tieBreaker, +}: CreateListItemsBulkOptions): Promise => { + // It causes errors if you try to add items to bulk that do not exist within ES + if (!value.length) { + return; + } + const body = value.reduce>( + (accum, singleValue, index) => { + const createdAt = dateNow ?? new Date().toISOString(); + const tieBreakerId = + tieBreaker != null && tieBreaker[index] != null ? tieBreaker[index] : uuid.v4(); + const elasticBody: IndexEsListItemSchema = { + created_at: createdAt, + created_by: user, + list_id: listId, + meta, + tie_breaker_id: tieBreakerId, + updated_at: createdAt, + updated_by: user, + ...transformListItemToElasticQuery({ type, value: singleValue }), + }; + const createBody: CreateEsBulkTypeSchema = { create: { _index: listItemIndex } }; + return [...accum, createBody, elasticBody]; + }, + [] + ); + + await callCluster('bulk', { + body, + index: listItemIndex, + }); +}; diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts new file mode 100644 index 0000000000000..00fcefb2c379f --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts @@ -0,0 +1,56 @@ +/* + * 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 { + LIST_ITEM_ID, + LIST_ITEM_INDEX, + getDeleteListItemOptionsMock, + getListItemResponseMock, +} from '../mocks'; + +import { getListItem } from './get_list_item'; +import { deleteListItem } from './delete_list_item'; + +jest.mock('./get_list_item', () => ({ + getListItem: jest.fn(), +})); + +describe('delete_list_item', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Delete returns a null if "getListItem" returns a null', async () => { + ((getListItem as unknown) as jest.Mock).mockResolvedValueOnce(null); + const options = getDeleteListItemOptionsMock(); + const deletedListItem = await deleteListItem(options); + expect(deletedListItem).toEqual(null); + }); + + test('Delete returns the same list item if a list item is returned from "getListItem"', async () => { + const listItem = getListItemResponseMock(); + ((getListItem as unknown) as jest.Mock).mockResolvedValueOnce(listItem); + const options = getDeleteListItemOptionsMock(); + const deletedListItem = await deleteListItem(options); + expect(deletedListItem).toEqual(listItem); + }); + + test('Delete calls "delete" if a list item is returned from "getListItem"', async () => { + const listItem = getListItemResponseMock(); + ((getListItem as unknown) as jest.Mock).mockResolvedValueOnce(listItem); + const options = getDeleteListItemOptionsMock(); + await deleteListItem(options); + const deleteQuery = { + id: LIST_ITEM_ID, + index: LIST_ITEM_INDEX, + }; + expect(options.callCluster).toBeCalledWith('delete', deleteQuery); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.ts new file mode 100644 index 0000000000000..9992f43387c89 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.ts @@ -0,0 +1,34 @@ +/* + * 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 { APICaller } from 'kibana/server'; + +import { Id, ListItemSchema } from '../../../common/schemas'; + +import { getListItem } from '.'; + +export interface DeleteListItemOptions { + id: Id; + callCluster: APICaller; + listItemIndex: string; +} + +export const deleteListItem = async ({ + id, + callCluster, + listItemIndex, +}: DeleteListItemOptions): Promise => { + const listItem = await getListItem({ callCluster, id, listItemIndex }); + if (listItem == null) { + return null; + } else { + await callCluster('delete', { + id, + index: listItemIndex, + }); + } + return listItem; +}; diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts new file mode 100644 index 0000000000000..c7c80638e4c37 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts @@ -0,0 +1,57 @@ +/* + * 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 { getDeleteListItemByValueOptionsMock, getListItemResponseMock } from '../mocks'; + +import { getListItemByValues } from './get_list_item_by_values'; +import { deleteListItemByValue } from './delete_list_item_by_value'; + +jest.mock('./get_list_item_by_values', () => ({ + getListItemByValues: jest.fn(), +})); + +describe('delete_list_item_by_value', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Delete returns a an empty array if the list items are also empty', async () => { + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([]); + const options = getDeleteListItemByValueOptionsMock(); + const deletedListItem = await deleteListItemByValue(options); + expect(deletedListItem).toEqual([]); + }); + + test('Delete returns the list item if a list item is returned from "getListByValues"', async () => { + const listItems = [getListItemResponseMock()]; + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce(listItems); + const options = getDeleteListItemByValueOptionsMock(); + const deletedListItem = await deleteListItemByValue(options); + expect(deletedListItem).toEqual(listItems); + }); + + test('Delete calls "deleteByQuery" if a list item is returned from "getListByValues"', async () => { + const listItems = [getListItemResponseMock()]; + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce(listItems); + const options = getDeleteListItemByValueOptionsMock(); + await deleteListItemByValue(options); + const deleteByQuery = { + body: { + query: { + bool: { + filter: [{ term: { list_id: 'some-list-id' } }, { terms: { ip: ['127.0.0.1'] } }], + }, + }, + }, + index: '.items', + }; + expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts new file mode 100644 index 0000000000000..ec29f14a0ff64 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts @@ -0,0 +1,53 @@ +/* + * 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 { APICaller } from 'kibana/server'; + +import { ListItemArraySchema, Type } from '../../../common/schemas'; +import { getQueryFilterFromTypeValue } from '../utils'; + +import { getListItemByValues } from './get_list_item_by_values'; + +export interface DeleteListItemByValueOptions { + listId: string; + type: Type; + value: string; + callCluster: APICaller; + listItemIndex: string; +} + +export const deleteListItemByValue = async ({ + listId, + value, + type, + callCluster, + listItemIndex, +}: DeleteListItemByValueOptions): Promise => { + const listItems = await getListItemByValues({ + callCluster, + listId, + listItemIndex, + type, + value: [value], + }); + const values = listItems.map(listItem => listItem.value); + const filter = getQueryFilterFromTypeValue({ + listId, + type, + value: values, + }); + await callCluster('deleteByQuery', { + body: { + query: { + bool: { + filter, + }, + }, + }, + index: listItemIndex, + }); + return listItems; +}; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item.test.ts new file mode 100644 index 0000000000000..31a421c2e31bf --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { + LIST_ID, + LIST_INDEX, + getCallClusterMock, + getListItemResponseMock, + getSearchListItemMock, +} from '../mocks'; + +import { getListItem } from './get_list_item'; + +describe('get_list_item', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list item as expected if the list item is found', async () => { + const data = getSearchListItemMock(); + const callCluster = getCallClusterMock(data); + const list = await getListItem({ callCluster, id: LIST_ID, listItemIndex: LIST_INDEX }); + const expected = getListItemResponseMock(); + expect(list).toEqual(expected); + }); + + test('it returns null if the search is empty', async () => { + const data = getSearchListItemMock(); + data.hits.hits = []; + const callCluster = getCallClusterMock(data); + const list = await getListItem({ callCluster, id: LIST_ID, listItemIndex: LIST_INDEX }); + expect(list).toEqual(null); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.ts b/x-pack/plugins/lists/server/services/items/get_list_item.ts new file mode 100644 index 0000000000000..83b30d336ccd4 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item.ts @@ -0,0 +1,43 @@ +/* + * 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 { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { Id, ListItemSchema, SearchEsListItemSchema } from '../../../common/schemas'; +import { deriveTypeFromItem, transformElasticToListItem } from '../utils'; + +interface GetListItemOptions { + id: Id; + callCluster: APICaller; + listItemIndex: string; +} + +export const getListItem = async ({ + id, + callCluster, + listItemIndex, +}: GetListItemOptions): Promise => { + const listItemES: SearchResponse = await callCluster('search', { + body: { + query: { + term: { + _id: id, + }, + }, + }, + ignoreUnavailable: true, + index: listItemIndex, + }); + + if (listItemES.hits.hits.length) { + const type = deriveTypeFromItem({ item: listItemES.hits.hits[0]._source }); + const listItems = transformElasticToListItem({ response: listItemES, type }); + return listItems[0]; + } else { + return null; + } +}; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.test.ts new file mode 100644 index 0000000000000..d30b3c795550f --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.test.ts @@ -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 { getListItemByValueOptionsMocks, getListItemResponseMock } from '../mocks'; + +import { getListItemByValues } from './get_list_item_by_values'; +import { getListItemByValue } from './get_list_item_by_value'; + +jest.mock('./get_list_item_by_values', () => ({ + getListItemByValues: jest.fn(), +})); + +describe('get_list_by_value', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Calls get_list_item_by_values with its input', async () => { + const listItemMock = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([listItemMock]); + const options = getListItemByValueOptionsMocks(); + const listItem = await getListItemByValue(options); + const expected = getListItemResponseMock(); + expect(listItem).toEqual([expected]); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts new file mode 100644 index 0000000000000..49bcf12043d7c --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts @@ -0,0 +1,34 @@ +/* + * 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 { APICaller } from 'kibana/server'; + +import { ListItemArraySchema, Type } from '../../../common/schemas'; + +import { getListItemByValues } from '.'; + +export interface GetListItemByValueOptions { + listId: string; + callCluster: APICaller; + listItemIndex: string; + type: Type; + value: string; +} + +export const getListItemByValue = async ({ + listId, + callCluster, + listItemIndex, + type, + value, +}: GetListItemByValueOptions): Promise => + getListItemByValues({ + callCluster, + listId, + listItemIndex, + type, + value: [value], + }); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts new file mode 100644 index 0000000000000..7f5fff4dc3147 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { + LIST_ID, + LIST_ITEM_INDEX, + TYPE, + VALUE, + VALUE_2, + getCallClusterMock, + getSearchListItemMock, +} from '../mocks'; + +import { getListItemByValues } from './get_list_item_by_values'; + +describe('get_list_item_by_values', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Returns a an empty array if the ES query is also empty', async () => { + const data = getSearchListItemMock(); + data.hits.hits = []; + const callCluster = getCallClusterMock(data); + const listItem = await getListItemByValues({ + callCluster, + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + type: TYPE, + value: [VALUE, VALUE_2], + }); + + expect(listItem).toEqual([]); + }); + + test('Returns transformed list item if the data exists within ES', async () => { + const data = getSearchListItemMock(); + const callCluster = getCallClusterMock(data); + const listItem = await getListItemByValues({ + callCluster, + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + type: TYPE, + value: [VALUE, VALUE_2], + }); + + expect(listItem).toEqual([ + { + created_at: '2020-04-20T15:25:31.830Z', + created_by: 'some user', + id: 'some-list-item-id', + list_id: 'some-list-id', + meta: {}, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + type: 'ip', + updated_at: '2020-04-20T15:25:31.830Z', + updated_by: 'some user', + value: '127.0.0.1', + }, + ]); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts new file mode 100644 index 0000000000000..29b9b01754027 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts @@ -0,0 +1,41 @@ +/* + * 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 { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; +import { getQueryFilterFromTypeValue, transformElasticToListItem } from '../utils'; + +export interface GetListItemByValuesOptions { + listId: string; + callCluster: APICaller; + listItemIndex: string; + type: Type; + value: string[]; +} + +export const getListItemByValues = async ({ + listId, + callCluster, + listItemIndex, + type, + value, +}: GetListItemByValuesOptions): Promise => { + const response: SearchResponse = await callCluster('search', { + body: { + query: { + bool: { + filter: getQueryFilterFromTypeValue({ listId, type, value }), + }, + }, + }, + ignoreUnavailable: true, + index: listItemIndex, + size: value.length, // This has a limit on the number which is 10k + }); + return transformElasticToListItem({ response, type }); +}; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_index.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_index.test.ts new file mode 100644 index 0000000000000..ffe2eff9f3ca7 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_index.test.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 { getListItemIndex } from './get_list_item_index'; + +describe('get_list_item_index', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Returns the list item index when there is a space', async () => { + const listIndex = getListItemIndex({ + listsItemsIndexName: 'lists-items-index', + spaceId: 'test-space', + }); + expect(listIndex).toEqual('lists-items-index-test-space'); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_index.ts b/x-pack/plugins/lists/server/services/items/get_list_item_index.ts new file mode 100644 index 0000000000000..4cd93df6d9bf4 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_index.ts @@ -0,0 +1,15 @@ +/* + * 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 interface GetListItemIndexOptions { + spaceId: string; + listsItemsIndexName: string; +} + +export const getListItemIndex = ({ + spaceId, + listsItemsIndexName, +}: GetListItemIndexOptions): string => `${listsItemsIndexName}-${spaceId}`; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_template.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_template.test.ts new file mode 100644 index 0000000000000..9c85fa6ff0256 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_template.test.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 { getListItemTemplate } from './get_list_item_template'; + +jest.mock('./list_item_mappings.json', () => ({ + listMappings: {}, +})); + +describe('get_list_item_template', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list template with the string filled in', async () => { + const template = getListItemTemplate('some_index'); + expect(template).toEqual({ + index_patterns: ['some_index-*'], + mappings: { listMappings: {} }, + settings: { index: { lifecycle: { name: 'some_index', rollover_alias: 'some_index' } } }, + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_template.ts b/x-pack/plugins/lists/server/services/items/get_list_item_template.ts new file mode 100644 index 0000000000000..95f4a09b40648 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_template.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 listsItemsMappings from './list_item_mappings.json'; + +export const getListItemTemplate = (index: string): Record => { + const template = { + index_patterns: [`${index}-*`], + mappings: listsItemsMappings, + settings: { + index: { + lifecycle: { + name: index, + rollover_alias: index, + }, + }, + }, + }; + return template; +}; diff --git a/x-pack/plugins/lists/server/services/items/index.ts b/x-pack/plugins/lists/server/services/items/index.ts new file mode 100644 index 0000000000000..ee1d83fabca31 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/index.ts @@ -0,0 +1,19 @@ +/* + * 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 * from './buffer_lines'; +export * from './create_list_item'; +export * from './create_list_items_bulk'; +export * from './delete_list_item_by_value'; +export * from './get_list_item_by_value'; +export * from './get_list_item'; +export * from './get_list_item_by_values'; +export * from './update_list_item'; +export * from './write_lines_to_bulk_list_items'; +export * from './write_list_items_to_stream'; +export * from './get_list_item_template'; +export * from './delete_list_item'; +export * from './get_list_item_index'; diff --git a/x-pack/plugins/lists/server/services/items/list_item_mappings.json b/x-pack/plugins/lists/server/services/items/list_item_mappings.json new file mode 100644 index 0000000000000..ca69c26df52b5 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/list_item_mappings.json @@ -0,0 +1,33 @@ +{ + "dynamic": "strict", + "properties": { + "tie_breaker_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "keyword": { + "type": "keyword" + }, + "meta": { + "enabled": "false", + "type": "object" + }, + "created_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } +} diff --git a/x-pack/plugins/lists/server/services/items/list_item_policy.json b/x-pack/plugins/lists/server/services/items/list_item_policy.json new file mode 100644 index 0000000000000..a4c84f73e7896 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/list_item_policy.json @@ -0,0 +1,14 @@ +{ + "policy": { + "phases": { + "hot": { + "min_age": "0ms", + "actions": { + "rollover": { + "max_size": "50gb" + } + } + } + } + } +} diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.test.ts b/x-pack/plugins/lists/server/services/items/update_list_item.test.ts new file mode 100644 index 0000000000000..4ef4110bc0742 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/update_list_item.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { getListItemResponseMock, getUpdateListItemOptionsMock } from '../mocks'; + +import { updateListItem } from './update_list_item'; +import { getListItem } from './get_list_item'; + +jest.mock('./get_list_item', () => ({ + getListItem: jest.fn(), +})); + +describe('update_list_item', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list item as expected with the id changed out for the elastic id when there is a list item to update', async () => { + const list = getListItemResponseMock(); + ((getListItem as unknown) as jest.Mock).mockResolvedValueOnce(list); + const options = getUpdateListItemOptionsMock(); + const updatedList = await updateListItem(options); + const expected = getListItemResponseMock(); + expected.id = 'elastic-id-123'; + expect(updatedList).toEqual(expected); + }); + + test('it returns null when there is not a list item to update', async () => { + ((getListItem as unknown) as jest.Mock).mockResolvedValueOnce(null); + const options = getUpdateListItemOptionsMock(); + const updatedList = await updateListItem(options); + expect(updatedList).toEqual(null); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts new file mode 100644 index 0000000000000..6a71b2a0caf41 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -0,0 +1,71 @@ +/* + * 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 { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { + Id, + ListItemSchema, + MetaOrUndefined, + UpdateEsListItemSchema, +} from '../../../common/schemas'; +import { transformListItemToElasticQuery } from '../utils'; + +import { getListItem } from './get_list_item'; + +export interface UpdateListItemOptions { + id: Id; + value: string | null | undefined; + callCluster: APICaller; + listItemIndex: string; + user: string; + meta: MetaOrUndefined; + dateNow?: string; +} + +export const updateListItem = async ({ + id, + value, + callCluster, + listItemIndex, + user, + meta, + dateNow, +}: UpdateListItemOptions): Promise => { + const updatedAt = dateNow ?? new Date().toISOString(); + const listItem = await getListItem({ callCluster, id, listItemIndex }); + if (listItem == null) { + return null; + } else { + const doc: UpdateEsListItemSchema = { + meta, + updated_at: updatedAt, + updated_by: user, + ...transformListItemToElasticQuery({ type: listItem.type, value: value ?? listItem.value }), + }; + + const response: CreateDocumentResponse = await callCluster('update', { + body: { + doc, + }, + id: listItem.id, + index: listItemIndex, + }); + return { + created_at: listItem.created_at, + created_by: listItem.created_by, + id: response._id, + list_id: listItem.list_id, + meta: meta ?? listItem.meta, + tie_breaker_id: listItem.tie_breaker_id, + type: listItem.type, + updated_at: updatedAt, + updated_by: listItem.updated_by, + value: value ?? listItem.value, + }; + } +}; diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts new file mode 100644 index 0000000000000..f064543f1ec93 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { + getImportListItemsToStreamOptionsMock, + getListItemResponseMock, + getWriteBufferToItemsOptionsMock, +} from '../mocks'; + +import { + LinesResult, + importListItemsToStream, + writeBufferToItems, +} from './write_lines_to_bulk_list_items'; + +import { getListItemByValues } from '.'; + +jest.mock('./get_list_item_by_values', () => ({ + getListItemByValues: jest.fn(), +})); + +describe('write_lines_to_bulk_list_items', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('importListItemsToStream', () => { + test('It imports a set of items to a write buffer by calling "getListItemByValues" with an empty buffer', async () => { + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([]); + const options = getImportListItemsToStreamOptionsMock(); + const promise = importListItemsToStream(options); + options.stream.push(null); + await promise; + expect(getListItemByValues).toBeCalledWith(expect.objectContaining({ value: [] })); + }); + + test('It imports a set of items to a write buffer by calling "getListItemByValues" with a single value given', async () => { + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([]); + const options = getImportListItemsToStreamOptionsMock(); + const promise = importListItemsToStream(options); + options.stream.push('127.0.0.1\n'); + options.stream.push(null); + await promise; + expect(getListItemByValues).toBeCalledWith(expect.objectContaining({ value: ['127.0.0.1'] })); + }); + + test('It imports a set of items to a write buffer by calling "getListItemByValues" with two values given', async () => { + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([]); + const options = getImportListItemsToStreamOptionsMock(); + const promise = importListItemsToStream(options); + options.stream.push('127.0.0.1\n'); + options.stream.push('127.0.0.2\n'); + options.stream.push(null); + await promise; + expect(getListItemByValues).toBeCalledWith( + expect.objectContaining({ value: ['127.0.0.1', '127.0.0.2'] }) + ); + }); + }); + + describe('writeBufferToItems', () => { + test('It returns no duplicates and no lines processed when given empty items', async () => { + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([]); + const options = getWriteBufferToItemsOptionsMock(); + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 0, + linesProcessed: 0, + }; + expect(linesResult).toEqual(expected); + }); + + test('It returns no lines processed when given items but no buffer', async () => { + const data = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([data]); + const options = getWriteBufferToItemsOptionsMock(); + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 0, + linesProcessed: 0, + }; + expect(linesResult).toEqual(expected); + }); + + test('It returns 1 lines processed when given a buffer item that is not a duplicate', async () => { + const data = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([data]); + const options = getWriteBufferToItemsOptionsMock(); + options.buffer = ['255.255.255.255']; + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 0, + linesProcessed: 1, + }; + expect(linesResult).toEqual(expected); + }); + + test('It filters a duplicate value out and reports it as a duplicate', async () => { + const data = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([data]); + const options = getWriteBufferToItemsOptionsMock(); + options.buffer = [data.value]; + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 1, + linesProcessed: 0, + }; + expect(linesResult).toEqual(expected); + }); + + test('It filters a duplicate value out and reports it as a duplicate and processing a second value as not a duplicate', async () => { + const data = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([data]); + const options = getWriteBufferToItemsOptionsMock(); + options.buffer = ['255.255.255.255', data.value]; + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 1, + linesProcessed: 1, + }; + expect(linesResult).toEqual(expected); + }); + + test('It filters a duplicate value out and reports it as a duplicate and processing two other values', async () => { + const data = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([data]); + const options = getWriteBufferToItemsOptionsMock(); + options.buffer = ['255.255.255.255', '192.168.0.1', data.value]; + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 1, + linesProcessed: 2, + }; + expect(linesResult).toEqual(expected); + }); + + test('It filters two duplicate values out and reports processes a single value', async () => { + const dataItem1 = getListItemResponseMock(); + dataItem1.value = '127.0.0.1'; + const dataItem2 = getListItemResponseMock(); + dataItem2.value = '127.0.0.2'; + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([dataItem1, dataItem2]); + const options = getWriteBufferToItemsOptionsMock(); + options.buffer = [dataItem1.value, dataItem2.value, '192.168.0.0.1']; + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 2, + linesProcessed: 1, + }; + expect(linesResult).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts new file mode 100644 index 0000000000000..542c2bb12d8e5 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts @@ -0,0 +1,102 @@ +/* + * 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 { Readable } from 'stream'; + +import { APICaller } from 'kibana/server'; + +import { MetaOrUndefined, Type } from '../../../common/schemas'; + +import { BufferLines } from './buffer_lines'; +import { getListItemByValues } from './get_list_item_by_values'; +import { createListItemsBulk } from './create_list_items_bulk'; + +export interface ImportListItemsToStreamOptions { + listId: string; + stream: Readable; + callCluster: APICaller; + listItemIndex: string; + type: Type; + user: string; + meta: MetaOrUndefined; +} + +export const importListItemsToStream = ({ + listId, + stream, + callCluster, + listItemIndex, + type, + user, + meta, +}: ImportListItemsToStreamOptions): Promise => { + return new Promise(resolve => { + const readBuffer = new BufferLines({ input: stream }); + readBuffer.on('lines', async (lines: string[]) => { + await writeBufferToItems({ + buffer: lines, + callCluster, + listId, + listItemIndex, + meta, + type, + user, + }); + }); + + readBuffer.on('close', () => { + resolve(); + }); + }); +}; + +export interface WriteBufferToItemsOptions { + listId: string; + callCluster: APICaller; + listItemIndex: string; + buffer: string[]; + type: Type; + user: string; + meta: MetaOrUndefined; +} + +export interface LinesResult { + linesProcessed: number; + duplicatesFound: number; +} + +export const writeBufferToItems = async ({ + listId, + callCluster, + listItemIndex, + buffer, + type, + user, + meta, +}: WriteBufferToItemsOptions): Promise => { + const items = await getListItemByValues({ + callCluster, + listId, + listItemIndex, + type, + value: buffer, + }); + const duplicatesRemoved = buffer.filter( + bufferedValue => !items.some(item => item.value === bufferedValue) + ); + const linesProcessed = duplicatesRemoved.length; + const duplicatesFound = buffer.length - duplicatesRemoved.length; + await createListItemsBulk({ + callCluster, + listId, + listItemIndex, + meta, + type, + user, + value: duplicatesRemoved, + }); + return { duplicatesFound, linesProcessed }; +}; diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts new file mode 100644 index 0000000000000..b08e5fa688b4b --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts @@ -0,0 +1,289 @@ +/* + * 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 { + LIST_ID, + LIST_ITEM_INDEX, + getCallClusterMock, + getExportListItemsToStreamOptionsMock, + getResponseOptionsMock, + getSearchListItemMock, + getWriteNextResponseOptions, + getWriteResponseHitsToStreamOptionsMock, +} from '../mocks'; + +import { + exportListItemsToStream, + getResponse, + getSearchAfterFromResponse, + writeNextResponse, + writeResponseHitsToStream, +} from '.'; + +describe('write_list_items_to_stream', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('exportListItemsToStream', () => { + test('It exports empty list items to the stream as an empty array', done => { + const options = getExportListItemsToStreamOptionsMock(); + const firstResponse = getSearchListItemMock(); + firstResponse.hits.hits = []; + options.callCluster = getCallClusterMock(firstResponse); + exportListItemsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.on('finish', () => { + expect(chunks).toEqual([]); + done(); + }); + }); + + test('It exports single list item to the stream', done => { + const options = getExportListItemsToStreamOptionsMock(); + exportListItemsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.on('finish', () => { + expect(chunks).toEqual(['127.0.0.1']); + done(); + }); + }); + + test('It exports two list items to the stream', done => { + const options = getExportListItemsToStreamOptionsMock(); + const firstResponse = getSearchListItemMock(); + const secondResponse = getSearchListItemMock(); + firstResponse.hits.hits = [...firstResponse.hits.hits, ...secondResponse.hits.hits]; + options.callCluster = getCallClusterMock(firstResponse); + exportListItemsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.on('finish', () => { + expect(chunks).toEqual(['127.0.0.1', '127.0.0.1']); + done(); + }); + }); + + test('It exports two list items to the stream with two separate calls', done => { + const options = getExportListItemsToStreamOptionsMock(); + + const firstResponse = getSearchListItemMock(); + firstResponse.hits.hits[0].sort = ['some-sort-value']; + + const secondResponse = getSearchListItemMock(); + secondResponse.hits.hits[0]._source.ip = '255.255.255.255'; + + options.callCluster = jest + .fn() + .mockResolvedValueOnce(firstResponse) + .mockResolvedValueOnce(secondResponse); + + exportListItemsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.on('finish', () => { + expect(chunks).toEqual(['127.0.0.1', '255.255.255.255']); + done(); + }); + }); + }); + + describe('writeNextResponse', () => { + test('It returns an empty searchAfter response when there is no sort defined', async () => { + const options = getWriteNextResponseOptions(); + const searchAfter = await writeNextResponse(options); + expect(searchAfter).toEqual(undefined); + }); + + test('It returns a searchAfter response when there is a sort defined', async () => { + const listItem = getSearchListItemMock(); + listItem.hits.hits[0].sort = ['sort-value-1']; + const options = getWriteNextResponseOptions(); + options.callCluster = getCallClusterMock(listItem); + const searchAfter = await writeNextResponse(options); + expect(searchAfter).toEqual(['sort-value-1']); + }); + + test('It returns a searchAfter response of undefined when the response is empty', async () => { + const listItem = getSearchListItemMock(); + listItem.hits.hits = []; + const options = getWriteNextResponseOptions(); + options.callCluster = getCallClusterMock(listItem); + const searchAfter = await writeNextResponse(options); + expect(searchAfter).toEqual(undefined); + }); + }); + + describe('getSearchAfterFromResponse', () => { + test('It returns undefined if the hits array is empty', () => { + const response = getSearchListItemMock(); + response.hits.hits = []; + const searchAfter = getSearchAfterFromResponse({ response }); + expect(searchAfter).toEqual(undefined); + }); + + test('It returns undefined if the hits array does not have a sort', () => { + const response = getSearchListItemMock(); + response.hits.hits[0].sort = undefined; + const searchAfter = getSearchAfterFromResponse({ response }); + expect(searchAfter).toEqual(undefined); + }); + + test('It returns a sort of a single array if that single item exists', () => { + const response = getSearchListItemMock(); + response.hits.hits[0].sort = ['sort-value-1', 'sort-value-2']; + const searchAfter = getSearchAfterFromResponse({ response }); + expect(searchAfter).toEqual(['sort-value-1', 'sort-value-2']); + }); + + test('It returns a sort of the last array element of size 2', () => { + const response = getSearchListItemMock(); + const response2 = getSearchListItemMock(); + response2.hits.hits[0].sort = ['sort-value']; + response.hits.hits = [...response.hits.hits, ...response2.hits.hits]; + const searchAfter = getSearchAfterFromResponse({ response }); + expect(searchAfter).toEqual(['sort-value']); + }); + }); + + describe('getResponse', () => { + test('It returns a simple response with the default size of 100', async () => { + const options = getResponseOptionsMock(); + options.searchAfter = ['string 1', 'string 2']; + await getResponse(options); + const expected = { + body: { + query: { term: { list_id: LIST_ID } }, + search_after: ['string 1', 'string 2'], + sort: [{ tie_breaker_id: 'asc' }], + }, + ignoreUnavailable: true, + index: LIST_ITEM_INDEX, + size: 100, + }; + expect(options.callCluster).toBeCalledWith('search', expected); + }); + + test('It returns a simple response with expected values and size changed', async () => { + const options = getResponseOptionsMock(); + options.searchAfter = ['string 1', 'string 2']; + options.size = 33; + await getResponse(options); + const expected = { + body: { + query: { term: { list_id: LIST_ID } }, + search_after: ['string 1', 'string 2'], + sort: [{ tie_breaker_id: 'asc' }], + }, + ignoreUnavailable: true, + index: LIST_ITEM_INDEX, + size: 33, + }; + expect(options.callCluster).toBeCalledWith('search', expected); + }); + }); + + describe('writeResponseHitsToStream', () => { + test('it will push into the stream the mock response', done => { + const options = getWriteResponseHitsToStreamOptionsMock(); + writeResponseHitsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.end(() => { + expect(chunks).toEqual(['127.0.0.1']); + done(); + }); + }); + + test('it will push into the stream an empty mock response', done => { + const options = getWriteResponseHitsToStreamOptionsMock(); + options.response.hits.hits = []; + writeResponseHitsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.on('finish', () => { + expect(chunks).toEqual([]); + done(); + }); + options.stream.end(); + }); + + test('it will push into the stream 2 mock responses', done => { + const options = getWriteResponseHitsToStreamOptionsMock(); + const secondResponse = getSearchListItemMock(); + options.response.hits.hits = [...options.response.hits.hits, ...secondResponse.hits.hits]; + writeResponseHitsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.end(() => { + expect(chunks).toEqual(['127.0.0.1', '127.0.0.1']); + done(); + }); + }); + + test('it will push an additional string given to it such as a new line character', done => { + const options = getWriteResponseHitsToStreamOptionsMock(); + const secondResponse = getSearchListItemMock(); + options.response.hits.hits = [...options.response.hits.hits, ...secondResponse.hits.hits]; + options.stringToAppend = '\n'; + writeResponseHitsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.end(() => { + expect(chunks).toEqual(['127.0.0.1\n', '127.0.0.1\n']); + done(); + }); + }); + + test('it will throw an exception with a status code if the hit_source is not a data type we expect', () => { + const options = getWriteResponseHitsToStreamOptionsMock(); + options.response.hits.hits[0]._source.ip = undefined; + options.response.hits.hits[0]._source.keyword = undefined; + const expected = `Encountered an error where hit._source was an unexpected type: ${JSON.stringify( + options.response.hits.hits[0]._source + )}`; + expect(() => writeResponseHitsToStream(options)).toThrow(expected); + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts new file mode 100644 index 0000000000000..b81e4a4fc73c2 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts @@ -0,0 +1,160 @@ +/* + * 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 { PassThrough } from 'stream'; + +import { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { SearchEsListItemSchema } from '../../../common/schemas'; +import { ErrorWithStatusCode } from '../../error_with_status_code'; + +/** + * How many results to page through from the network at a time + * using search_after + */ +export const SIZE = 100; + +export interface ExportListItemsToStreamOptions { + listId: string; + callCluster: APICaller; + listItemIndex: string; + stream: PassThrough; + stringToAppend: string | null | undefined; +} + +export const exportListItemsToStream = ({ + listId, + callCluster, + stream, + listItemIndex, + stringToAppend, +}: ExportListItemsToStreamOptions): void => { + // Use a timeout to start the reading process on the next tick. + // and prevent the async await from bubbling up to the caller + setTimeout(async () => { + let searchAfter = await writeNextResponse({ + callCluster, + listId, + listItemIndex, + searchAfter: undefined, + stream, + stringToAppend, + }); + while (searchAfter != null) { + searchAfter = await writeNextResponse({ + callCluster, + listId, + listItemIndex, + searchAfter, + stream, + stringToAppend, + }); + } + stream.end(); + }); +}; + +export interface WriteNextResponseOptions { + listId: string; + callCluster: APICaller; + listItemIndex: string; + stream: PassThrough; + searchAfter: string[] | undefined; + stringToAppend: string | null | undefined; +} + +export const writeNextResponse = async ({ + listId, + callCluster, + stream, + listItemIndex, + searchAfter, + stringToAppend, +}: WriteNextResponseOptions): Promise => { + const response = await getResponse({ + callCluster, + listId, + listItemIndex, + searchAfter, + }); + + if (response.hits.hits.length) { + writeResponseHitsToStream({ response, stream, stringToAppend }); + return getSearchAfterFromResponse({ response }); + } else { + return undefined; + } +}; + +export const getSearchAfterFromResponse = ({ + response, +}: { + response: SearchResponse; +}): string[] | undefined => + response.hits.hits.length > 0 + ? response.hits.hits[response.hits.hits.length - 1].sort + : undefined; + +export interface GetResponseOptions { + callCluster: APICaller; + listId: string; + searchAfter: undefined | string[]; + listItemIndex: string; + size?: number; +} + +export const getResponse = async ({ + callCluster, + searchAfter, + listId, + listItemIndex, + size = SIZE, +}: GetResponseOptions): Promise> => { + return callCluster('search', { + body: { + query: { + term: { + list_id: listId, + }, + }, + search_after: searchAfter, + sort: [{ tie_breaker_id: 'asc' }], + }, + ignoreUnavailable: true, + index: listItemIndex, + size, + }); +}; + +export interface WriteResponseHitsToStreamOptions { + response: SearchResponse; + stream: PassThrough; + stringToAppend: string | null | undefined; +} + +export const writeResponseHitsToStream = ({ + response, + stream, + stringToAppend, +}: WriteResponseHitsToStreamOptions): void => { + const stringToAppendOrEmpty = stringToAppend ?? ''; + + response.hits.hits.forEach(hit => { + if (hit._source.ip != null) { + stream.push(`${hit._source.ip}${stringToAppendOrEmpty}`); + } else if (hit._source.keyword != null) { + stream.push(`${hit._source.keyword}${stringToAppendOrEmpty}`); + } else { + throw new ErrorWithStatusCode( + `Encountered an error where hit._source was an unexpected type: ${JSON.stringify( + hit._source + )}`, + 400 + ); + } + }); +}; diff --git a/x-pack/plugins/lists/server/services/lists/client.ts b/x-pack/plugins/lists/server/services/lists/client.ts new file mode 100644 index 0000000000000..ba22bf72cc18c --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/client.ts @@ -0,0 +1,413 @@ +/* + * 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 { APICaller } from 'kibana/server'; + +import { ListItemArraySchema, ListItemSchema, ListSchema } from '../../../common/schemas'; +import { ConfigType } from '../../config'; +import { + createList, + deleteList, + getList, + getListIndex, + getListTemplate, + updateList, +} from '../../services/lists'; +import { + createListItem, + deleteListItem, + deleteListItemByValue, + exportListItemsToStream, + getListItem, + getListItemByValue, + getListItemByValues, + getListItemIndex, + getListItemTemplate, + importListItemsToStream, + updateListItem, +} from '../../services/items'; +import { + createBootstrapIndex, + deleteAllIndex, + deletePolicy, + deleteTemplate, + getIndexExists, + getPolicyExists, + getTemplateExists, + setPolicy, + setTemplate, +} from '../../siem_server_deps'; +import listsItemsPolicy from '../items/list_item_policy.json'; + +import listPolicy from './list_policy.json'; +import { + ConstructorOptions, + CreateListIfItDoesNotExistOptions, + CreateListItemOptions, + CreateListOptions, + DeleteListItemByValueOptions, + DeleteListItemOptions, + DeleteListOptions, + ExportListItemsToStreamOptions, + GetListItemByValueOptions, + GetListItemOptions, + GetListItemsByValueOptions, + GetListOptions, + ImportListItemsToStreamOptions, + UpdateListItemOptions, + UpdateListOptions, +} from './client_types'; + +export class ListClient { + private readonly spaceId: string; + private readonly user: string; + private readonly config: ConfigType; + private readonly callCluster: APICaller; + + constructor({ spaceId, user, config, callCluster }: ConstructorOptions) { + this.spaceId = spaceId; + this.user = user; + this.config = config; + this.callCluster = callCluster; + } + + public getListIndex = (): string => { + const { + spaceId, + config: { listIndex: listsIndexName }, + } = this; + return getListIndex({ listsIndexName, spaceId }); + }; + + public getListItemIndex = (): string => { + const { + spaceId, + config: { listItemIndex: listsItemsIndexName }, + } = this; + return getListItemIndex({ listsItemsIndexName, spaceId }); + }; + + public getList = async ({ id }: GetListOptions): Promise => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return getList({ callCluster, id, listIndex }); + }; + + public createList = async ({ + id, + name, + description, + type, + meta, + }: CreateListOptions): Promise => { + const { callCluster, user } = this; + const listIndex = this.getListIndex(); + return createList({ callCluster, description, id, listIndex, meta, name, type, user }); + }; + + public createListIfItDoesNotExist = async ({ + id, + name, + description, + type, + meta, + }: CreateListIfItDoesNotExistOptions): Promise => { + const list = await this.getList({ id }); + if (list == null) { + return this.createList({ description, id, meta, name, type }); + } else { + return list; + } + }; + + public getListIndexExists = async (): Promise => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return getIndexExists(callCluster, listIndex); + }; + + public getListItemIndexExists = async (): Promise => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return getIndexExists(callCluster, listItemIndex); + }; + + public createListBootStrapIndex = async (): Promise => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return createBootstrapIndex(callCluster, listIndex); + }; + + public createListItemBootStrapIndex = async (): Promise => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return createBootstrapIndex(callCluster, listItemIndex); + }; + + public getListPolicyExists = async (): Promise => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return getPolicyExists(callCluster, listIndex); + }; + + public getListItemPolicyExists = async (): Promise => { + const { callCluster } = this; + const listsItemIndex = this.getListItemIndex(); + return getPolicyExists(callCluster, listsItemIndex); + }; + + public getListTemplateExists = async (): Promise => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return getTemplateExists(callCluster, listIndex); + }; + + public getListItemTemplateExists = async (): Promise => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return getTemplateExists(callCluster, listItemIndex); + }; + + public getListTemplate = (): Record => { + const listIndex = this.getListIndex(); + return getListTemplate(listIndex); + }; + + public getListItemTemplate = (): Record => { + const listItemIndex = this.getListItemIndex(); + return getListItemTemplate(listItemIndex); + }; + + public setListTemplate = async (): Promise => { + const { callCluster } = this; + const template = this.getListTemplate(); + const listIndex = this.getListIndex(); + return setTemplate(callCluster, listIndex, template); + }; + + public setListItemTemplate = async (): Promise => { + const { callCluster } = this; + const template = this.getListItemTemplate(); + const listItemIndex = this.getListItemIndex(); + return setTemplate(callCluster, listItemIndex, template); + }; + + public setListPolicy = async (): Promise => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return setPolicy(callCluster, listIndex, listPolicy); + }; + + public setListItemPolicy = async (): Promise => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return setPolicy(callCluster, listItemIndex, listsItemsPolicy); + }; + + public deleteListIndex = async (): Promise => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return deleteAllIndex(callCluster, `${listIndex}-*`); + }; + + public deleteListItemIndex = async (): Promise => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return deleteAllIndex(callCluster, `${listItemIndex}-*`); + }; + + public deleteListPolicy = async (): Promise => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return deletePolicy(callCluster, listIndex); + }; + + public deleteListItemPolicy = async (): Promise => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return deletePolicy(callCluster, listItemIndex); + }; + + public deleteListTemplate = async (): Promise => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return deleteTemplate(callCluster, listIndex); + }; + + public deleteListItemTemplate = async (): Promise => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return deleteTemplate(callCluster, listItemIndex); + }; + + public deleteListItem = async ({ id }: DeleteListItemOptions): Promise => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return deleteListItem({ callCluster, id, listItemIndex }); + }; + + public deleteListItemByValue = async ({ + listId, + value, + type, + }: DeleteListItemByValueOptions): Promise => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return deleteListItemByValue({ + callCluster, + listId, + listItemIndex, + type, + value, + }); + }; + + public deleteList = async ({ id }: DeleteListOptions): Promise => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + const listItemIndex = this.getListItemIndex(); + return deleteList({ + callCluster, + id, + listIndex, + listItemIndex, + }); + }; + + public exportListItemsToStream = ({ + stringToAppend, + listId, + stream, + }: ExportListItemsToStreamOptions): void => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + exportListItemsToStream({ + callCluster, + listId, + listItemIndex, + stream, + stringToAppend, + }); + }; + + public importListItemsToStream = async ({ + type, + listId, + stream, + meta, + }: ImportListItemsToStreamOptions): Promise => { + const { callCluster, user } = this; + const listItemIndex = this.getListItemIndex(); + return importListItemsToStream({ + callCluster, + listId, + listItemIndex, + meta, + stream, + type, + user, + }); + }; + + public getListItemByValue = async ({ + listId, + value, + type, + }: GetListItemByValueOptions): Promise => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return getListItemByValue({ + callCluster, + listId, + listItemIndex, + type, + value, + }); + }; + + public createListItem = async ({ + id, + listId, + value, + type, + meta, + }: CreateListItemOptions): Promise => { + const { callCluster, user } = this; + const listItemIndex = this.getListItemIndex(); + return createListItem({ + callCluster, + id, + listId, + listItemIndex, + meta, + type, + user, + value, + }); + }; + + public updateListItem = async ({ + id, + value, + meta, + }: UpdateListItemOptions): Promise => { + const { callCluster, user } = this; + const listItemIndex = this.getListItemIndex(); + return updateListItem({ + callCluster, + id, + listItemIndex, + meta, + user, + value, + }); + }; + + public updateList = async ({ + id, + name, + description, + meta, + }: UpdateListOptions): Promise => { + const { callCluster, user } = this; + const listIndex = this.getListIndex(); + return updateList({ + callCluster, + description, + id, + listIndex, + meta, + name, + user, + }); + }; + + public getListItem = async ({ id }: GetListItemOptions): Promise => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return getListItem({ + callCluster, + id, + listItemIndex, + }); + }; + + public getListItemByValues = async ({ + type, + listId, + value, + }: GetListItemsByValueOptions): Promise => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return getListItemByValues({ + callCluster, + listId, + listItemIndex, + type, + value, + }); + }; +} diff --git a/x-pack/plugins/lists/server/services/lists/client_types.ts b/x-pack/plugins/lists/server/services/lists/client_types.ts new file mode 100644 index 0000000000000..2cc58c02dbfcf --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/client_types.ts @@ -0,0 +1,115 @@ +/* + * 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 { PassThrough, Readable } from 'stream'; + +import { APICaller, KibanaRequest } from 'kibana/server'; + +import { SecurityPluginSetup } from '../../../../security/server'; +import { + Description, + DescriptionOrUndefined, + Id, + IdOrUndefined, + MetaOrUndefined, + Name, + NameOrUndefined, + Type, +} from '../../../common/schemas'; +import { ConfigType } from '../../config'; + +export interface ConstructorOptions { + callCluster: APICaller; + config: ConfigType; + request: KibanaRequest; + spaceId: string; + user: string; + security: SecurityPluginSetup | undefined | null; +} + +export interface GetListOptions { + id: Id; +} + +export interface DeleteListOptions { + id: Id; +} + +export interface DeleteListItemOptions { + id: Id; +} + +export interface CreateListOptions { + id: IdOrUndefined; + name: Name; + description: Description; + type: Type; + meta: MetaOrUndefined; +} + +export interface CreateListIfItDoesNotExistOptions { + id: Id; + name: Name; + description: Description; + type: Type; + meta: MetaOrUndefined; +} + +export interface DeleteListItemByValueOptions { + listId: string; + value: string; + type: Type; +} + +export interface GetListItemByValueOptions { + listId: string; + value: string; + type: Type; +} + +export interface ExportListItemsToStreamOptions { + stringToAppend: string | null | undefined; + listId: string; + stream: PassThrough; +} + +export interface ImportListItemsToStreamOptions { + listId: string; + type: Type; + stream: Readable; + meta: MetaOrUndefined; +} + +export interface CreateListItemOptions { + id: IdOrUndefined; + listId: string; + type: Type; + value: string; + meta: MetaOrUndefined; +} + +export interface UpdateListItemOptions { + id: Id; + value: string | null | undefined; + meta: MetaOrUndefined; +} + +export interface UpdateListOptions { + id: Id; + name: NameOrUndefined; + description: DescriptionOrUndefined; + meta: MetaOrUndefined; +} + +export interface GetListItemOptions { + id: Id; +} + +export interface GetListItemsByValueOptions { + type: Type; + listId: string; + value: string[]; +} diff --git a/x-pack/plugins/lists/server/services/lists/create_list.test.ts b/x-pack/plugins/lists/server/services/lists/create_list.test.ts new file mode 100644 index 0000000000000..36284a70fb97d --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/create_list.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { + LIST_ID, + LIST_INDEX, + getCreateListOptionsMock, + getIndexESListMock, + getListResponseMock, +} from '../mocks'; + +import { createList } from './create_list'; + +describe('crete_list', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list as expected with the id changed out for the elastic id', async () => { + const options = getCreateListOptionsMock(); + const list = await createList(options); + const expected = getListResponseMock(); + expected.id = 'elastic-id-123'; + expect(list).toEqual(expected); + }); + + test('It calls "callCluster" with body, index, and listIndex', async () => { + const options = getCreateListOptionsMock(); + await createList(options); + const body = getIndexESListMock(); + const expected = { + body, + id: LIST_ID, + index: LIST_INDEX, + }; + expect(options.callCluster).toBeCalledWith('index', expected); + }); + + test('It returns an auto-generated id if id is sent in undefined', async () => { + const options = getCreateListOptionsMock(); + options.id = undefined; + const list = await createList(options); + const expected = getListResponseMock(); + expected.id = 'elastic-id-123'; + expect(list).toEqual(expected); + }); +}); diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts new file mode 100644 index 0000000000000..ddbc99c88a877 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -0,0 +1,67 @@ +/* + * 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 uuid from 'uuid'; +import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { + Description, + IdOrUndefined, + IndexEsListSchema, + ListSchema, + MetaOrUndefined, + Name, + Type, +} from '../../../common/schemas'; + +export interface CreateListOptions { + id: IdOrUndefined; + type: Type; + name: Name; + description: Description; + callCluster: APICaller; + listIndex: string; + user: string; + meta: MetaOrUndefined; + dateNow?: string; + tieBreaker?: string; +} + +export const createList = async ({ + id, + name, + type, + description, + callCluster, + listIndex, + user, + meta, + dateNow, + tieBreaker, +}: CreateListOptions): Promise => { + const createdAt = dateNow ?? new Date().toISOString(); + const body: IndexEsListSchema = { + created_at: createdAt, + created_by: user, + description, + meta, + name, + tie_breaker_id: tieBreaker ?? uuid.v4(), + type, + updated_at: createdAt, + updated_by: user, + }; + const response: CreateDocumentResponse = await callCluster('index', { + body, + id, + index: listIndex, + }); + return { + id: response._id, + ...body, + }; +}; diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts new file mode 100644 index 0000000000000..62b5e7c7aec4a --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { + LIST_ID, + LIST_INDEX, + LIST_ITEM_INDEX, + getDeleteListOptionsMock, + getListResponseMock, +} from '../mocks'; + +import { getList } from './get_list'; +import { deleteList } from './delete_list'; + +jest.mock('./get_list', () => ({ + getList: jest.fn(), +})); + +describe('delete_list', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Delete returns a null if the list is also null', async () => { + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(null); + const options = getDeleteListOptionsMock(); + const deletedList = await deleteList(options); + expect(deletedList).toEqual(null); + }); + + test('Delete returns the list if a list is returned from getList', async () => { + const list = getListResponseMock(); + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(list); + const options = getDeleteListOptionsMock(); + const deletedList = await deleteList(options); + expect(deletedList).toEqual(list); + }); + + test('Delete calls "deleteByQuery" and "delete" if a list is returned from getList', async () => { + const list = getListResponseMock(); + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(list); + const options = getDeleteListOptionsMock(); + await deleteList(options); + const deleteByQuery = { + body: { query: { term: { list_id: LIST_ID } } }, + index: LIST_ITEM_INDEX, + }; + expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery); + }); + + test('Delete calls "delete" second if a list is returned from getList', async () => { + const list = getListResponseMock(); + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(list); + const options = getDeleteListOptionsMock(); + await deleteList(options); + const deleteQuery = { + id: LIST_ID, + index: LIST_INDEX, + }; + expect(options.callCluster).toHaveBeenNthCalledWith(2, 'delete', deleteQuery); + }); + + test('Delete does not call data client if the list returns null', async () => { + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(null); + const options = getDeleteListOptionsMock(); + await deleteList(options); + expect(options.callCluster).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.ts b/x-pack/plugins/lists/server/services/lists/delete_list.ts new file mode 100644 index 0000000000000..bc66c88b082a3 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/delete_list.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 { APICaller } from 'kibana/server'; + +import { Id, ListSchema } from '../../../common/schemas'; + +import { getList } from './get_list'; + +export interface DeleteListOptions { + id: Id; + callCluster: APICaller; + listIndex: string; + listItemIndex: string; +} + +export const deleteList = async ({ + id, + callCluster, + listIndex, + listItemIndex, +}: DeleteListOptions): Promise => { + const list = await getList({ callCluster, id, listIndex }); + if (list == null) { + return null; + } else { + await callCluster('deleteByQuery', { + body: { + query: { + term: { + list_id: id, + }, + }, + }, + index: listItemIndex, + }); + + await callCluster('delete', { + id, + index: listIndex, + }); + return list; + } +}; diff --git a/x-pack/plugins/lists/server/services/lists/get_list.test.ts b/x-pack/plugins/lists/server/services/lists/get_list.test.ts new file mode 100644 index 0000000000000..c997d5325296a --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { + LIST_ID, + LIST_INDEX, + getCallClusterMock, + getListResponseMock, + getSearchListMock, +} from '../mocks'; + +import { getList } from './get_list'; + +describe('get_list', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list as expected if the list is found', async () => { + const data = getSearchListMock(); + const callCluster = getCallClusterMock(data); + const list = await getList({ callCluster, id: LIST_ID, listIndex: LIST_INDEX }); + const expected = getListResponseMock(); + expect(list).toEqual(expected); + }); + + test('it returns null if the search is empty', async () => { + const data = getSearchListMock(); + data.hits.hits = []; + const callCluster = getCallClusterMock(data); + const list = await getList({ callCluster, id: LIST_ID, listIndex: LIST_INDEX }); + expect(list).toEqual(null); + }); +}); diff --git a/x-pack/plugins/lists/server/services/lists/get_list.ts b/x-pack/plugins/lists/server/services/lists/get_list.ts new file mode 100644 index 0000000000000..c04bd504ad8c0 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list.ts @@ -0,0 +1,42 @@ +/* + * 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 { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { Id, ListSchema, SearchEsListSchema } from '../../../common/schemas'; + +interface GetListOptions { + id: Id; + callCluster: APICaller; + listIndex: string; +} + +export const getList = async ({ + id, + callCluster, + listIndex, +}: GetListOptions): Promise => { + const result: SearchResponse = await callCluster('search', { + body: { + query: { + term: { + _id: id, + }, + }, + }, + ignoreUnavailable: true, + index: listIndex, + }); + if (result.hits.hits.length) { + return { + id: result.hits.hits[0]._id, + ...result.hits.hits[0]._source, + }; + } else { + return null; + } +}; diff --git a/x-pack/plugins/lists/server/services/lists/get_list_index.test.ts b/x-pack/plugins/lists/server/services/lists/get_list_index.test.ts new file mode 100644 index 0000000000000..f82928ffeddd2 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list_index.test.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 { getListIndex } from './get_list_index'; + +describe('get_list_index', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Returns the list index when there is a space', async () => { + const listIndex = getListIndex({ + listsIndexName: 'lists-index', + spaceId: 'test-space', + }); + expect(listIndex).toEqual('lists-index-test-space'); + }); +}); diff --git a/x-pack/plugins/lists/server/services/lists/get_list_index.ts b/x-pack/plugins/lists/server/services/lists/get_list_index.ts new file mode 100644 index 0000000000000..5086603fa8403 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list_index.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. + */ + +interface GetListIndexOptions { + spaceId: string; + listsIndexName: string; +} + +export const getListIndex = ({ spaceId, listsIndexName }: GetListIndexOptions): string => + `${listsIndexName}-${spaceId}`; diff --git a/x-pack/plugins/lists/server/services/lists/get_list_template.test.ts b/x-pack/plugins/lists/server/services/lists/get_list_template.test.ts new file mode 100644 index 0000000000000..e25eaaafd855e --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list_template.test.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 { getListTemplate } from './get_list_template'; + +jest.mock('./list_mappings.json', () => ({ + listMappings: {}, +})); + +describe('get_list_template', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list template with the string filled in', async () => { + const template = getListTemplate('some_index'); + expect(template).toEqual({ + index_patterns: ['some_index-*'], + mappings: { listMappings: {} }, + settings: { index: { lifecycle: { name: 'some_index', rollover_alias: 'some_index' } } }, + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/lists/get_list_template.ts b/x-pack/plugins/lists/server/services/lists/get_list_template.ts new file mode 100644 index 0000000000000..9d93a051f2d10 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list_template.ts @@ -0,0 +1,20 @@ +/* + * 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 listMappings from './list_mappings.json'; + +export const getListTemplate = (index: string): Record => ({ + index_patterns: [`${index}-*`], + mappings: listMappings, + settings: { + index: { + lifecycle: { + name: index, + rollover_alias: index, + }, + }, + }, +}); diff --git a/x-pack/plugins/lists/server/services/lists/index.ts b/x-pack/plugins/lists/server/services/lists/index.ts new file mode 100644 index 0000000000000..f704ef0b05b82 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/index.ts @@ -0,0 +1,12 @@ +/* + * 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 * from './create_list'; +export * from './delete_list'; +export * from './get_list'; +export * from './get_list_template'; +export * from './update_list'; +export * from './get_list_index'; diff --git a/x-pack/plugins/lists/server/services/lists/list_mappings.json b/x-pack/plugins/lists/server/services/lists/list_mappings.json new file mode 100644 index 0000000000000..1136a53da787d --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/list_mappings.json @@ -0,0 +1,33 @@ +{ + "dynamic": "strict", + "properties": { + "name": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "meta": { + "enabled": "false", + "type": "object" + }, + "created_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } +} diff --git a/x-pack/plugins/lists/server/services/lists/list_policy.json b/x-pack/plugins/lists/server/services/lists/list_policy.json new file mode 100644 index 0000000000000..a4c84f73e7896 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/list_policy.json @@ -0,0 +1,14 @@ +{ + "policy": { + "phases": { + "hot": { + "min_age": "0ms", + "actions": { + "rollover": { + "max_size": "50gb" + } + } + } + } + } +} diff --git a/x-pack/plugins/lists/server/services/lists/update_list.test.ts b/x-pack/plugins/lists/server/services/lists/update_list.test.ts new file mode 100644 index 0000000000000..09bf0ee69c981 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/update_list.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { getListResponseMock, getUpdateListOptionsMock } from '../mocks'; + +import { updateList } from './update_list'; +import { getList } from './get_list'; + +jest.mock('./get_list', () => ({ + getList: jest.fn(), +})); + +describe('update_list', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list as expected with the id changed out for the elastic id when there is a list to update', async () => { + const list = getListResponseMock(); + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(list); + const options = getUpdateListOptionsMock(); + const updatedList = await updateList(options); + const expected = getListResponseMock(); + expected.id = 'elastic-id-123'; + expect(updatedList).toEqual(expected); + }); + + test('it returns null when there is not a list to update', async () => { + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(null); + const options = getUpdateListOptionsMock(); + const updatedList = await updateList(options); + expect(updatedList).toEqual(null); + }); +}); diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts new file mode 100644 index 0000000000000..9859adf062485 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -0,0 +1,72 @@ +/* + * 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 { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { + DescriptionOrUndefined, + Id, + ListSchema, + MetaOrUndefined, + NameOrUndefined, + UpdateEsListSchema, +} from '../../../common/schemas'; + +import { getList } from '.'; + +export interface UpdateListOptions { + id: Id; + callCluster: APICaller; + listIndex: string; + user: string; + name: NameOrUndefined; + description: DescriptionOrUndefined; + meta: MetaOrUndefined; + dateNow?: string; +} + +export const updateList = async ({ + id, + name, + description, + callCluster, + listIndex, + user, + meta, + dateNow, +}: UpdateListOptions): Promise => { + const updatedAt = dateNow ?? new Date().toISOString(); + const list = await getList({ callCluster, id, listIndex }); + if (list == null) { + return null; + } else { + const doc: UpdateEsListSchema = { + description, + meta, + name, + updated_at: updatedAt, + updated_by: user, + }; + const response: CreateDocumentResponse = await callCluster('update', { + body: { doc }, + id, + index: listIndex, + }); + return { + created_at: list.created_at, + created_by: list.created_by, + description: description ?? list.description, + id: response._id, + meta, + name: name ?? list.name, + tie_breaker_id: list.tie_breaker_id, + type: list.type, + updated_at: updatedAt, + updated_by: user, + }; + } +}; diff --git a/x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts new file mode 100644 index 0000000000000..180ecbb797339 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.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 { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { LIST_INDEX } from './lists_services_mock_constants'; +import { getShardMock } from './get_shard_mock'; + +export const getEmptyCreateDocumentResponseMock = (): CreateDocumentResponse => ({ + _id: 'elastic-id-123', + _index: LIST_INDEX, + _shards: getShardMock(), + _type: '', + _version: 1, + created: true, + result: '', +}); + +export const getCallClusterMock = ( + callCluster: unknown = getEmptyCreateDocumentResponseMock() +): APICaller => jest.fn().mockResolvedValue(callCluster); diff --git a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts new file mode 100644 index 0000000000000..fcdad66d65251 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.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 { CreateListItemsBulkOptions } from '../items'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { + DATE_NOW, + LIST_ID, + LIST_ITEM_INDEX, + META, + TIE_BREAKERS, + TYPE, + USER, + VALUE, + VALUE_2, +} from './lists_services_mock_constants'; + +export const getCreateListItemBulkOptionsMock = (): CreateListItemsBulkOptions => ({ + callCluster: getCallClusterMock(), + dateNow: DATE_NOW, + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + meta: META, + tieBreaker: TIE_BREAKERS, + type: TYPE, + user: USER, + value: [VALUE, VALUE_2], +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts new file mode 100644 index 0000000000000..17e3ad2f8de08 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.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 { CreateListItemOptions } from '../items'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { + DATE_NOW, + LIST_ID, + LIST_ITEM_ID, + LIST_ITEM_INDEX, + META, + TIE_BREAKER, + TYPE, + USER, +} from './lists_services_mock_constants'; + +export const getCreateListItemOptionsMock = (): CreateListItemOptions => ({ + callCluster: getCallClusterMock(), + dateNow: DATE_NOW, + id: LIST_ITEM_ID, + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + meta: META, + tieBreaker: TIE_BREAKER, + type: TYPE, + user: USER, + value: '127.0.0.1', +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts new file mode 100644 index 0000000000000..0ea6533fc122a --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts @@ -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 { CreateListOptions } from '../lists'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { + DATE_NOW, + DESCRIPTION, + LIST_ID, + LIST_INDEX, + META, + NAME, + TIE_BREAKER, + TYPE, + USER, +} from './lists_services_mock_constants'; + +export const getCreateListOptionsMock = (): CreateListOptions => ({ + callCluster: getCallClusterMock(), + dateNow: DATE_NOW, + description: DESCRIPTION, + id: LIST_ID, + listIndex: LIST_INDEX, + meta: META, + name: NAME, + tieBreaker: TIE_BREAKER, + type: TYPE, + user: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts new file mode 100644 index 0000000000000..f6859e72d71b3 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DeleteListItemByValueOptions } from '../items'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from './lists_services_mock_constants'; + +export const getDeleteListItemByValueOptionsMock = (): DeleteListItemByValueOptions => ({ + callCluster: getCallClusterMock(), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + type: TYPE, + value: VALUE, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts new file mode 100644 index 0000000000000..271c185860b07 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.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 { DeleteListItemOptions } from '../items'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { LIST_ITEM_ID, LIST_ITEM_INDEX } from './lists_services_mock_constants'; + +export const getDeleteListItemOptionsMock = (): DeleteListItemOptions => ({ + callCluster: getCallClusterMock(), + id: LIST_ITEM_ID, + listItemIndex: LIST_ITEM_INDEX, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts new file mode 100644 index 0000000000000..8ec92dfa4ef77 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.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 { DeleteListOptions } from '../lists'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { LIST_ID, LIST_INDEX, LIST_ITEM_INDEX } from './lists_services_mock_constants'; + +export const getDeleteListOptionsMock = (): DeleteListOptions => ({ + callCluster: getCallClusterMock(), + id: LIST_ID, + listIndex: LIST_INDEX, + listItemIndex: LIST_ITEM_INDEX, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts new file mode 100644 index 0000000000000..d7541f3e09e6c --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts @@ -0,0 +1,20 @@ +/* + * 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 { ImportListItemsToStreamOptions } from '../items'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from './lists_services_mock_constants'; +import { TestReadable } from './test_readable'; + +export const getImportListItemsToStreamOptionsMock = (): ImportListItemsToStreamOptions => ({ + callCluster: getCallClusterMock(), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + meta: META, + stream: new TestReadable(), + type: TYPE, + user: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_index_es_list_item_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_index_es_list_item_mock.ts new file mode 100644 index 0000000000000..574e4afcb36f0 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_index_es_list_item_mock.ts @@ -0,0 +1,20 @@ +/* + * 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 { IndexEsListItemSchema } from '../../../common/schemas'; + +import { DATE_NOW, LIST_ID, META, TIE_BREAKER, USER, VALUE } from './lists_services_mock_constants'; + +export const getIndexESListItemMock = (ip = VALUE): IndexEsListItemSchema => ({ + created_at: DATE_NOW, + created_by: USER, + ip, + list_id: LIST_ID, + meta: META, + tie_breaker_id: TIE_BREAKER, + updated_at: DATE_NOW, + updated_by: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_index_es_list_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_index_es_list_mock.ts new file mode 100644 index 0000000000000..4e4d8d9c572e4 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_index_es_list_mock.ts @@ -0,0 +1,29 @@ +/* + * 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 { IndexEsListSchema } from '../../../common/schemas'; + +import { + DATE_NOW, + DESCRIPTION, + META, + NAME, + TIE_BREAKER, + TYPE, + USER, +} from './lists_services_mock_constants'; + +export const getIndexESListMock = (): IndexEsListSchema => ({ + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, + meta: META, + name: NAME, + tie_breaker_id: TIE_BREAKER, + type: TYPE, + updated_at: DATE_NOW, + updated_by: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts new file mode 100644 index 0000000000000..96bc22ca7e6f2 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GetListItemByValueOptions } from '../items'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from './lists_services_mock_constants'; + +export const getListItemByValueOptionsMocks = (): GetListItemByValueOptions => ({ + callCluster: getCallClusterMock(), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + type: TYPE, + value: VALUE, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts new file mode 100644 index 0000000000000..f21f97dc8d15f --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GetListItemByValuesOptions } from '../items'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2 } from './lists_services_mock_constants'; + +export const getListItemByValuesOptionsMocks = (): GetListItemByValuesOptions => ({ + callCluster: getCallClusterMock(), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + type: TYPE, + value: [VALUE, VALUE_2], +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_item_response_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_list_item_response_mock.ts new file mode 100644 index 0000000000000..1a30282ddaeba --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_list_item_response_mock.ts @@ -0,0 +1,22 @@ +/* + * 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 { ListItemSchema } from '../../../common/schemas'; + +import { DATE_NOW, LIST_ID, LIST_ITEM_ID, USER, VALUE } from './lists_services_mock_constants'; + +export const getListItemResponseMock = (): ListItemSchema => ({ + created_at: DATE_NOW, + created_by: USER, + id: LIST_ITEM_ID, + list_id: LIST_ID, + meta: {}, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + type: 'ip', + updated_at: DATE_NOW, + updated_by: USER, + value: VALUE, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_response_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_list_response_mock.ts new file mode 100644 index 0000000000000..ea068d774c4ed --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_list_response_mock.ts @@ -0,0 +1,22 @@ +/* + * 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 { ListSchema } from '../../../common/schemas'; + +import { DATE_NOW, DESCRIPTION, LIST_ID, NAME, USER } from './lists_services_mock_constants'; + +export const getListResponseMock = (): ListSchema => ({ + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, + id: LIST_ID, + meta: {}, + name: NAME, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + type: 'ip', + updated_at: DATE_NOW, + updated_by: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_es_list_item_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_search_es_list_item_mock.ts new file mode 100644 index 0000000000000..5e9fd8995c0eb --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_search_es_list_item_mock.ts @@ -0,0 +1,21 @@ +/* + * 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 { SearchEsListItemSchema } from '../../../common/schemas'; + +import { DATE_NOW, LIST_ID, USER, VALUE } from './lists_services_mock_constants'; + +export const getSearchEsListItemMock = (): SearchEsListItemSchema => ({ + created_at: DATE_NOW, + created_by: USER, + ip: VALUE, + keyword: undefined, + list_id: LIST_ID, + meta: {}, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + updated_at: DATE_NOW, + updated_by: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_es_list_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_search_es_list_mock.ts new file mode 100644 index 0000000000000..6a565437617ba --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_search_es_list_mock.ts @@ -0,0 +1,21 @@ +/* + * 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 { SearchEsListSchema } from '../../../common/schemas'; + +import { DATE_NOW, DESCRIPTION, NAME, USER } from './lists_services_mock_constants'; + +export const getSearchEsListMock = (): SearchEsListSchema => ({ + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, + meta: {}, + name: NAME, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + type: 'ip', + updated_at: DATE_NOW, + updated_by: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_list_item_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_search_list_item_mock.ts new file mode 100644 index 0000000000000..9f877c8168cca --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_search_list_item_mock.ts @@ -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 { SearchResponse } from 'elasticsearch'; + +import { SearchEsListItemSchema } from '../../../common/schemas'; + +import { getShardMock } from './get_shard_mock'; +import { LIST_INDEX, LIST_ITEM_ID } from './lists_services_mock_constants'; +import { getSearchEsListItemMock } from './get_search_es_list_item_mock'; + +export const getSearchListItemMock = (): SearchResponse => ({ + _scroll_id: '123', + _shards: getShardMock(), + hits: { + hits: [ + { + _id: LIST_ITEM_ID, + _index: LIST_INDEX, + _score: 0, + _source: getSearchEsListItemMock(), + _type: '', + }, + ], + max_score: 0, + total: 1, + }, + timed_out: false, + took: 10, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_list_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_search_list_mock.ts new file mode 100644 index 0000000000000..9728139eab42a --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_search_list_mock.ts @@ -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 { SearchResponse } from 'elasticsearch'; + +import { SearchEsListSchema } from '../../../common/schemas'; + +import { getShardMock } from './get_shard_mock'; +import { LIST_ID, LIST_INDEX } from './lists_services_mock_constants'; +import { getSearchEsListMock } from './get_search_es_list_mock'; + +export const getSearchListMock = (): SearchResponse => ({ + _scroll_id: '123', + _shards: getShardMock(), + hits: { + hits: [ + { + _id: LIST_ID, + _index: LIST_INDEX, + _score: 0, + _source: getSearchEsListMock(), + _type: '', + }, + ], + max_score: 0, + total: 1, + }, + timed_out: false, + took: 10, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_shard_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_shard_mock.ts new file mode 100644 index 0000000000000..4cc6577d5e531 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_shard_mock.ts @@ -0,0 +1,14 @@ +/* + * 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 { ShardsResponse } from 'elasticsearch'; + +export const getShardMock = (): ShardsResponse => ({ + failed: 0, + skipped: 0, + successful: 0, + total: 0, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts new file mode 100644 index 0000000000000..0555997941baa --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts @@ -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 { UpdateListItemOptions } from '../items'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { + DATE_NOW, + LIST_ITEM_ID, + LIST_ITEM_INDEX, + META, + USER, + VALUE, +} from './lists_services_mock_constants'; + +export const getUpdateListItemOptionsMock = (): UpdateListItemOptions => ({ + callCluster: getCallClusterMock(), + dateNow: DATE_NOW, + id: LIST_ITEM_ID, + listItemIndex: LIST_ITEM_INDEX, + meta: META, + user: USER, + value: VALUE, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts new file mode 100644 index 0000000000000..fe6fc37eaf81e --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts @@ -0,0 +1,28 @@ +/* + * 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 { UpdateListOptions } from '../lists'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { + DATE_NOW, + DESCRIPTION, + LIST_ID, + LIST_INDEX, + META, + NAME, + USER, +} from './lists_services_mock_constants'; + +export const getUpdateListOptionsMock = (): UpdateListOptions => ({ + callCluster: getCallClusterMock(), + dateNow: DATE_NOW, + description: DESCRIPTION, + id: LIST_ID, + listIndex: LIST_INDEX, + meta: META, + name: NAME, + user: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts new file mode 100644 index 0000000000000..d6b7d70c1aa77 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts @@ -0,0 +1,19 @@ +/* + * 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 { WriteBufferToItemsOptions } from '../items'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from './lists_services_mock_constants'; + +export const getWriteBufferToItemsOptionsMock = (): WriteBufferToItemsOptions => ({ + buffer: [], + callCluster: getCallClusterMock(), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + meta: META, + type: TYPE, + user: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts new file mode 100644 index 0000000000000..c945818a83e8a --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts @@ -0,0 +1,49 @@ +/* + * 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 { Stream } from 'stream'; + +import { + ExportListItemsToStreamOptions, + GetResponseOptions, + WriteNextResponseOptions, + WriteResponseHitsToStreamOptions, +} from '../items'; + +import { LIST_ID, LIST_ITEM_INDEX } from './lists_services_mock_constants'; +import { getSearchListItemMock } from './get_search_list_item_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; + +export const getExportListItemsToStreamOptionsMock = (): ExportListItemsToStreamOptions => ({ + callCluster: getCallClusterMock(getSearchListItemMock()), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + stream: new Stream.PassThrough(), + stringToAppend: undefined, +}); + +export const getWriteNextResponseOptions = (): WriteNextResponseOptions => ({ + callCluster: getCallClusterMock(getSearchListItemMock()), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + searchAfter: [], + stream: new Stream.PassThrough(), + stringToAppend: undefined, +}); + +export const getResponseOptionsMock = (): GetResponseOptions => ({ + callCluster: getCallClusterMock(), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + searchAfter: [], + size: 100, +}); + +export const getWriteResponseHitsToStreamOptionsMock = (): WriteResponseHitsToStreamOptions => ({ + response: getSearchListItemMock(), + stream: new Stream.PassThrough(), + stringToAppend: undefined, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/index.ts b/x-pack/plugins/lists/server/services/mocks/index.ts new file mode 100644 index 0000000000000..c555ba322fa2b --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/index.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. + */ + +export * from './get_call_cluster_mock'; +export * from './get_delete_list_options_mock'; +export * from './get_create_list_options_mock'; +export * from './get_list_response_mock'; +export * from './get_search_list_mock'; +export * from './get_shard_mock'; +export * from './lists_services_mock_constants'; +export * from './get_update_list_options_mock'; +export * from './get_create_list_item_options_mock'; +export * from './get_list_item_response_mock'; +export * from './get_index_es_list_mock'; +export * from './get_index_es_list_item_mock'; +export * from './get_create_list_item_bulk_options_mock'; +export * from './get_delete_list_item_by_value_options_mock'; +export * from './get_delete_list_item_options_mock'; +export * from './get_list_item_by_values_options_mock'; +export * from './get_search_es_list_mock'; +export * from './get_search_es_list_item_mock'; +export * from './get_list_item_by_value_options_mock'; +export * from './get_update_list_item_options_mock'; +export * from './get_write_buffer_to_items_options_mock'; +export * from './get_import_list_items_to_stream_options_mock'; +export * from './get_write_list_items_to_stream_options_mock'; +export * from './get_search_list_item_mock'; +export * from './test_readable'; diff --git a/x-pack/plugins/lists/server/services/mocks/lists_services_mock_constants.ts b/x-pack/plugins/lists/server/services/mocks/lists_services_mock_constants.ts new file mode 100644 index 0000000000000..d174211f348ea --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/lists_services_mock_constants.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. + */ +export const DATE_NOW = '2020-04-20T15:25:31.830Z'; +export const USER = 'some user'; +export const LIST_INDEX = '.lists'; +export const LIST_ITEM_INDEX = '.items'; +export const NAME = 'some name'; +export const DESCRIPTION = 'some description'; +export const LIST_ID = 'some-list-id'; +export const LIST_ITEM_ID = 'some-list-item-id'; +export const TIE_BREAKER = '6a76b69d-80df-4ab2-8c3e-85f466b06a0e'; +export const TIE_BREAKERS = [ + '21530991-4051-46ec-bc35-2afa09a1b0b5', + '3c662054-ae37-4aa9-9936-3e8e2ea26775', + '60e49a20-3a23-48b6-8bf9-ed5e3b70f7a0', + '38814080-a40f-4358-992a-3b875f9b7dec', + '29fa61be-aaaf-411c-a78a-7059e3f723f1', + '9c19c959-cb9d-4cd2-99e4-1ea2baf0ef0e', + 'd409308c-f94b-4b3a-8234-bbd7a80c9140', + '87824c99-cd83-45c4-8aa6-4ad95dfea62c', + '7b940c17-9355-479f-b882-f3e575718f79', + '5983ad0c-4ef4-4fa0-8308-80ab9ecc4f74', +]; +export const META = {}; +export const TYPE = 'ip'; +export const VALUE = '127.0.0.1'; +export const VALUE_2 = '255.255.255'; diff --git a/x-pack/plugins/lists/server/services/mocks/test_readable.ts b/x-pack/plugins/lists/server/services/mocks/test_readable.ts new file mode 100644 index 0000000000000..52ad6de484005 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/test_readable.ts @@ -0,0 +1,11 @@ +/* + * 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 { Readable } from 'stream'; + +export class TestReadable extends Readable { + public _read(): void {} +} diff --git a/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts new file mode 100644 index 0000000000000..3b6f58479a2f2 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { getSearchEsListItemMock } from '../mocks'; +import { Type } from '../../../common/schemas'; + +import { deriveTypeFromItem } from './derive_type_from_es_type'; + +describe('derive_type_from_es_type', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns the item ip if it exists', () => { + const item = getSearchEsListItemMock(); + const derivedType = deriveTypeFromItem({ item }); + const expected: Type = 'ip'; + expect(derivedType).toEqual(expected); + }); + + test('it returns the item keyword if it exists', () => { + const item = getSearchEsListItemMock(); + item.ip = undefined; + item.keyword = 'some keyword'; + const derivedType = deriveTypeFromItem({ item }); + const expected: Type = 'keyword'; + expect(derivedType).toEqual(expected); + }); + + test('it throws an error with status code if neither one exists', () => { + const item = getSearchEsListItemMock(); + item.ip = undefined; + item.keyword = undefined; + const expected = `Was expecting a valid type from the Elastic Search List Item such as ip or keyword but did not found one here ${JSON.stringify( + item + )}`; + expect(() => deriveTypeFromItem({ item })).toThrowError(expected); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.ts b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.ts new file mode 100644 index 0000000000000..7a65e74bf4947 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.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 { SearchEsListItemSchema, Type } from '../../../common/schemas'; +import { ErrorWithStatusCode } from '../../error_with_status_code'; + +interface DeriveTypeFromItemOptions { + item: SearchEsListItemSchema; +} + +export const deriveTypeFromItem = ({ item }: DeriveTypeFromItemOptions): Type => { + if (item.ip != null) { + return 'ip'; + } else if (item.keyword != null) { + return 'keyword'; + } else { + throw new ErrorWithStatusCode( + `Was expecting a valid type from the Elastic Search List Item such as ip or keyword but did not found one here ${JSON.stringify( + item + )}`, + 400 + ); + } +}; diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts new file mode 100644 index 0000000000000..3d48e44e26eaa --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts @@ -0,0 +1,92 @@ +/* + * 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 { QueryFilterType, getQueryFilterFromTypeValue } from './get_query_filter_from_type_value'; + +describe('get_query_filter_from_type_value', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns an ip if given an ip', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'ip', + value: ['127.0.0.1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { terms: { ip: ['127.0.0.1'] } }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it returns two ip if given two ip', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'ip', + value: ['127.0.0.1', '127.0.0.2'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { terms: { ip: ['127.0.0.1', '127.0.0.2'] } }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it returns a keyword if given a keyword', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'keyword', + value: ['host-name-1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { terms: { keyword: ['host-name-1'] } }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it returns two keywords if given two values', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'keyword', + value: ['host-name-1', 'host-name-2'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { terms: { keyword: ['host-name-1', 'host-name-2'] } }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it returns an empty keyword given an empty value', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'keyword', + value: [], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { terms: { keyword: [] } }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it returns an empty ip given an empty value', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'ip', + value: [], + }); + const expected: QueryFilterType = [{ term: { list_id: 'list-123' } }, { terms: { ip: [] } }]; + expect(queryFilter).toEqual(expected); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts new file mode 100644 index 0000000000000..3f50efe0c6c56 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts @@ -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 { Type } from '../../../common/schemas'; + +export type QueryFilterType = Array< + { term: { list_id: string } } | { terms: { ip: string[] } } | { terms: { keyword: string[] } } +>; + +export const getQueryFilterFromTypeValue = ({ + type, + value, + listId, +}: { + type: Type; + value: string[]; + listId: string; + // We disable the consistent return since we want to use typescript for exhaustive type checks + // eslint-disable-next-line consistent-return +}): QueryFilterType => { + const filter: QueryFilterType = [{ term: { list_id: listId } }]; + switch (type) { + case 'ip': { + return [...filter, ...[{ terms: { ip: value } }]]; + } + case 'keyword': { + return [...filter, ...[{ terms: { keyword: value } }]]; + } + } +}; diff --git a/x-pack/plugins/lists/server/services/utils/index.ts b/x-pack/plugins/lists/server/services/utils/index.ts new file mode 100644 index 0000000000000..8a44b5ab607bf --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/index.ts @@ -0,0 +1,10 @@ +/* + * 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 * from './get_query_filter_from_type_value'; +export * from './transform_elastic_to_list_item'; +export * from './transform_list_item_to_elastic_query'; +export * from './derive_type_from_es_type'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts new file mode 100644 index 0000000000000..3b9864be6df53 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { ListItemArraySchema } from '../../../common/schemas'; +import { getListItemResponseMock, getSearchListItemMock } from '../mocks'; + +import { transformElasticToListItem } from './transform_elastic_to_list_item'; + +describe('transform_elastic_to_list_item', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it transforms an elastic type to a list item type', () => { + const response = getSearchListItemMock(); + const queryFilter = transformElasticToListItem({ + response, + type: 'ip', + }); + const expected: ListItemArraySchema = [getListItemResponseMock()]; + expect(queryFilter).toEqual(expected); + }); + + test('it transforms an elastic keyword type to a list item type', () => { + const response = getSearchListItemMock(); + response.hits.hits[0]._source.ip = undefined; + response.hits.hits[0]._source.keyword = 'host-name-example'; + const queryFilter = transformElasticToListItem({ + response, + type: 'keyword', + }); + const listItemResponse = getListItemResponseMock(); + listItemResponse.type = 'keyword'; + listItemResponse.value = 'host-name-example'; + const expected: ListItemArraySchema = [listItemResponse]; + expect(queryFilter).toEqual(expected); + }); + + test('it does a throw if it cannot determine the list item type from "ip"', () => { + const response = getSearchListItemMock(); + response.hits.hits[0]._source.ip = undefined; + response.hits.hits[0]._source.keyword = 'host-name-example'; + expect(() => + transformElasticToListItem({ + response, + type: 'ip', + }) + ).toThrow('Was expecting ip to not be null/undefined'); + }); + + test('it does a throw if it cannot determine the list item type from "keyword"', () => { + const response = getSearchListItemMock(); + response.hits.hits[0]._source.ip = '127.0.0.1'; + response.hits.hits[0]._source.keyword = undefined; + expect(() => + transformElasticToListItem({ + response, + type: 'keyword', + }) + ).toThrow('Was expecting keyword to not be null/undefined'); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts new file mode 100644 index 0000000000000..2dc0f4fe7a821 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts @@ -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 { SearchResponse } from 'elasticsearch'; + +import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; +import { ErrorWithStatusCode } from '../../error_with_status_code'; + +export interface TransformElasticToListItemOptions { + response: SearchResponse; + type: Type; +} + +export const transformElasticToListItem = ({ + response, + type, +}: TransformElasticToListItemOptions): ListItemArraySchema => { + return response.hits.hits.map(hit => { + const { + _id, + _source: { + created_at, + updated_at, + updated_by, + created_by, + list_id, + tie_breaker_id, + ip, + keyword, + meta, + }, + } = hit; + + const baseTypes = { + created_at, + created_by, + id: _id, + list_id, + meta, + tie_breaker_id, + type, + updated_at, + updated_by, + }; + + switch (type) { + case 'ip': { + if (ip == null) { + throw new ErrorWithStatusCode('Was expecting ip to not be null/undefined', 400); + } + return { + ...baseTypes, + value: ip, + }; + } + case 'keyword': { + if (keyword == null) { + throw new ErrorWithStatusCode('Was expecting keyword to not be null/undefined', 400); + } + return { + ...baseTypes, + value: keyword, + }; + } + } + return assertUnreachable(); + }); +}; + +const assertUnreachable = (): never => { + throw new Error('Unknown type in elastic_to_list_items'); +}; diff --git a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts new file mode 100644 index 0000000000000..217cad30bfdbb --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.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 { EsDataTypeUnion, Type } from '../../../common/schemas'; + +import { transformListItemToElasticQuery } from './transform_list_item_to_elastic_query'; + +describe('transform_elastic_to_elastic_query', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it transforms a ip type and value to a union', () => { + const elasticQuery = transformListItemToElasticQuery({ + type: 'ip', + value: '127.0.0.1', + }); + const expected: EsDataTypeUnion = { ip: '127.0.0.1' }; + expect(elasticQuery).toEqual(expected); + }); + + test('it transforms a keyword type and value to a union', () => { + const elasticQuery = transformListItemToElasticQuery({ + type: 'keyword', + value: 'host-name', + }); + const expected: EsDataTypeUnion = { keyword: 'host-name' }; + expect(elasticQuery).toEqual(expected); + }); + + test('it throws if the type is not known', () => { + const type: Type = 'made-up' as Type; + expect(() => + transformListItemToElasticQuery({ + type, + value: 'some-value', + }) + ).toThrow('Unknown type: "made-up" in transformListItemToElasticQuery'); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts new file mode 100644 index 0000000000000..051802cc41b5b --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts @@ -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 { EsDataTypeUnion, Type } from '../../../common/schemas'; + +export const transformListItemToElasticQuery = ({ + type, + value, +}: { + type: Type; + value: string; +}): EsDataTypeUnion => { + switch (type) { + case 'ip': { + return { + ip: value, + }; + } + case 'keyword': { + return { + keyword: value, + }; + } + } + return assertUnreachable(type); +}; + +const assertUnreachable = (type: string): never => { + throw new Error(`Unknown type: "${type}" in transformListItemToElasticQuery`); +}; diff --git a/x-pack/plugins/lists/server/siem_server_deps.ts b/x-pack/plugins/lists/server/siem_server_deps.ts new file mode 100644 index 0000000000000..e78debc8e4349 --- /dev/null +++ b/x-pack/plugins/lists/server/siem_server_deps.ts @@ -0,0 +1,21 @@ +/* + * 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 { + transformError, + buildSiemResponse, +} from '../../siem/server/lib/detection_engine/routes/utils'; +export { deleteTemplate } from '../../siem/server/lib/detection_engine/index/delete_template'; +export { deletePolicy } from '../../siem/server/lib/detection_engine/index/delete_policy'; +export { deleteAllIndex } from '../../siem/server/lib/detection_engine/index/delete_all_index'; +export { setPolicy } from '../../siem/server/lib/detection_engine/index/set_policy'; +export { setTemplate } from '../../siem/server/lib/detection_engine/index/set_template'; +export { getTemplateExists } from '../../siem/server/lib/detection_engine/index/get_template_exists'; +export { getPolicyExists } from '../../siem/server/lib/detection_engine/index/get_policy_exists'; +export { createBootstrapIndex } from '../../siem/server/lib/detection_engine/index/create_bootstrap_index'; +export { getIndexExists } from '../../siem/server/lib/detection_engine/index/get_index_exists'; +export { buildRouteValidation } from '../../siem/server/utils/build_validation/route_validation'; +export { validate } from '../../siem/server/lib/detection_engine/routes/rules/validate'; diff --git a/x-pack/plugins/lists/server/types.ts b/x-pack/plugins/lists/server/types.ts new file mode 100644 index 0000000000000..e0e4495d47c34 --- /dev/null +++ b/x-pack/plugins/lists/server/types.ts @@ -0,0 +1,28 @@ +/* + * 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 { IContextProvider, RequestHandler } from 'kibana/server'; + +import { SecurityPluginSetup } from '../../security/server'; +import { SpacesPluginSetup } from '../../spaces/server'; + +import { ListClient } from './services/lists/client'; + +export type ContextProvider = IContextProvider, 'lists'>; + +export interface PluginsSetup { + security: SecurityPluginSetup | undefined | null; + spaces: SpacesPluginSetup | undefined | null; +} + +export type ContextProviderReturn = Promise<{ getListClient: () => ListClient }>; +declare module 'src/core/server' { + interface RequestHandlerContext { + lists?: { + getListClient: () => ListClient; + }; + } +} diff --git a/x-pack/plugins/maps/public/actions/map_actions.d.ts b/x-pack/plugins/maps/public/actions/map_actions.d.ts index c8db284a5c4f1..38c56405787eb 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.d.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.d.ts @@ -74,3 +74,11 @@ export function updateMapSetting( settingKey: string, settingValue: string | boolean | number ): AnyAction; + +export function cloneLayer(layerId: string): AnyAction; + +export function fitToLayerExtent(layerId: string): AnyAction; + +export function removeLayer(layerId: string): AnyAction; + +export function toggleLayerVisible(layerId: string): AnyAction; diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap index 27ea52bfed044..f1cb1a8864753 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap @@ -9,12 +9,10 @@ exports[`TOCEntry is rendered 1`] = `
-
-
-
-
-
{ - dispatch(toggleLayerVisible(layerId)); - }, - fitToBounds: layerId => { - dispatch(fitToLayerExtent(layerId)); - }, - cloneLayer: layerId => { - dispatch(cloneLayer(layerId)); - }, - removeLayer: layerId => { - dispatch(removeLayer(layerId)); - }, hideTOCDetails: layerId => { dispatch(hideTOCDetails(layerId)); }, diff --git a/x-pack/plugins/maps/public/components/__snapshots__/layer_toc_actions.test.js.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap similarity index 92% rename from x-pack/plugins/maps/public/components/__snapshots__/layer_toc_actions.test.js.snap rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap index af836ceffa4b7..b8c652909408a 100644 --- a/x-pack/plugins/maps/public/components/__snapshots__/layer_toc_actions.test.js.snap +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`LayerTocActions is rendered 1`] = ` +exports[`TOCEntryActionsPopover is rendered 1`] = ` , "name": "Hide layer", "onClick": [Function], + "toolTipContent": null, }, Object { "data-test-subj": "editLayerButton", + "disabled": false, "icon": , "name": "Edit layer", "onClick": [Function], + "toolTipContent": null, }, Object { "data-test-subj": "cloneLayerButton", @@ -104,6 +107,7 @@ exports[`LayerTocActions is rendered 1`] = ` />, "name": "Clone layer", "onClick": [Function], + "toolTipContent": null, }, Object { "data-test-subj": "removeLayerButton", @@ -113,6 +117,7 @@ exports[`LayerTocActions is rendered 1`] = ` />, "name": "Remove layer", "onClick": [Function], + "toolTipContent": null, }, ], "title": "Layer actions", @@ -123,7 +128,7 @@ exports[`LayerTocActions is rendered 1`] = ` `; -exports[`LayerTocActions should disable fit to data when supportsFitToBounds is false 1`] = ` +exports[`TOCEntryActionsPopover should disable fit to data when supportsFitToBounds is false 1`] = ` , "name": "Hide layer", "onClick": [Function], + "toolTipContent": null, }, Object { "data-test-subj": "editLayerButton", + "disabled": false, "icon": , "name": "Edit layer", "onClick": [Function], + "toolTipContent": null, }, Object { "data-test-subj": "cloneLayerButton", @@ -227,6 +235,7 @@ exports[`LayerTocActions should disable fit to data when supportsFitToBounds is />, "name": "Clone layer", "onClick": [Function], + "toolTipContent": null, }, Object { "data-test-subj": "removeLayerButton", @@ -236,6 +245,7 @@ exports[`LayerTocActions should disable fit to data when supportsFitToBounds is />, "name": "Remove layer", "onClick": [Function], + "toolTipContent": null, }, ], "title": "Layer actions", @@ -246,7 +256,7 @@ exports[`LayerTocActions should disable fit to data when supportsFitToBounds is `; -exports[`LayerTocActions should not show edit actions in read only mode 1`] = ` +exports[`TOCEntryActionsPopover should not show edit actions in read only mode 1`] = ` , "name": "Hide layer", "onClick": [Function], + "toolTipContent": null, }, ], "title": "Layer actions", diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts new file mode 100644 index 0000000000000..1437370557efc --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts @@ -0,0 +1,49 @@ +/* + * 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 { AnyAction, Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { MapStoreState } from '../../../../../../reducers/store'; +import { + fitToLayerExtent, + toggleLayerVisible, + cloneLayer, + removeLayer, +} from '../../../../../../actions/map_actions'; +import { getMapZoom, isUsingSearch } from '../../../../../../selectors/map_selectors'; +import { getIsReadOnly } from '../../../../../../selectors/ui_selectors'; +import { TOCEntryActionsPopover } from './toc_entry_actions_popover'; + +function mapStateToProps(state: MapStoreState) { + return { + isReadOnly: getIsReadOnly(state), + isUsingSearch: isUsingSearch(state), + zoom: getMapZoom(state), + }; +} + +function mapDispatchToProps(dispatch: Dispatch) { + return { + cloneLayer: (layerId: string) => { + dispatch(cloneLayer(layerId)); + }, + fitToBounds: (layerId: string) => { + dispatch(fitToLayerExtent(layerId)); + }, + removeLayer: (layerId: string) => { + dispatch(removeLayer(layerId)); + }, + toggleVisible: (layerId: string) => { + dispatch(toggleLayerVisible(layerId)); + }, + }; +} + +const connectedTOCEntryActionsPopover = connect( + mapStateToProps, + mapDispatchToProps +)(TOCEntryActionsPopover); +export { connectedTOCEntryActionsPopover as TOCEntryActionsPopover }; diff --git a/x-pack/plugins/maps/public/components/layer_toc_actions.test.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx similarity index 52% rename from x-pack/plugins/maps/public/components/layer_toc_actions.test.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx index c3a8f59c4c736..b873119fd7d13 100644 --- a/x-pack/plugins/maps/public/components/layer_toc_actions.test.js +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx @@ -3,21 +3,45 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable max-classes-per-file */ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { AbstractLayer, ILayer } from '../../../../../../layers/layer'; +import { AbstractSource, ISource } from '../../../../../../layers/sources/source'; +import { AbstractStyle, IStyle } from '../../../../../../layers/styles/style'; -import { LayerTocActions } from './layer_toc_actions'; +import { TOCEntryActionsPopover } from './toc_entry_actions_popover'; -let supportsFitToBounds; -const layerMock = { - supportsFitToBounds: () => { +let supportsFitToBounds: boolean; + +class MockSource extends AbstractSource implements ISource {} + +class MockStyle extends AbstractStyle implements IStyle {} + +class LayerMock extends AbstractLayer implements ILayer { + constructor() { + const sourceDescriptor = { + type: 'mySourceType', + }; + const source = new MockSource(sourceDescriptor); + const style = new MockStyle({ type: 'myStyleType' }); + const layerDescriptor = { + id: 'testLayer', + sourceDescriptor, + }; + super({ layerDescriptor, source, style }); + } + + async supportsFitToBounds(): Promise { return supportsFitToBounds; - }, - isVisible: () => { + } + + isVisible() { return true; - }, - getIconAndTooltipContent: (zoom, isUsingSearch) => { + } + + getIconAndTooltipContent(zoom: number, isUsingSearch: boolean) { return { icon: mockIcon, tooltipContent: `simulated tooltip content at zoom: ${zoom}`, @@ -28,24 +52,31 @@ const layerMock = { }, ], }; - }, -}; + } +} const defaultProps = { + cloneLayer: () => {}, displayName: 'layer 1', + editLayer: () => {}, escapedDisplayName: 'layer1', - zoom: 0, - layer: layerMock, + fitToBounds: () => {}, + isEditButtonDisabled: false, + isReadOnly: false, isUsingSearch: true, + layer: new LayerMock(), + removeLayer: () => {}, + toggleVisible: () => {}, + zoom: 0, }; -describe('LayerTocActions', () => { +describe('TOCEntryActionsPopover', () => { beforeEach(() => { supportsFitToBounds = true; }); test('is rendered', async () => { - const component = shallowWithIntl(); + const component = shallowWithIntl(); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -56,7 +87,9 @@ describe('LayerTocActions', () => { }); test('should not show edit actions in read only mode', async () => { - const component = shallowWithIntl(); + const component = shallowWithIntl( + + ); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -68,7 +101,7 @@ describe('LayerTocActions', () => { test('should disable fit to data when supportsFitToBounds is false', async () => { supportsFitToBounds = false; - const component = shallowWithIntl(); + const component = shallowWithIntl(); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); diff --git a/x-pack/plugins/maps/public/components/layer_toc_actions.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx similarity index 81% rename from x-pack/plugins/maps/public/components/layer_toc_actions.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx index d79eda16037cb..d628cca61de11 100644 --- a/x-pack/plugins/maps/public/components/layer_toc_actions.js +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx @@ -8,8 +8,31 @@ import React, { Component, Fragment } from 'react'; import { EuiButtonEmpty, EuiPopover, EuiContextMenu, EuiIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { ILayer } from '../../../../../../layers/layer'; + +interface Props { + cloneLayer: (layerId: string) => void; + displayName: string; + editLayer: () => void; + escapedDisplayName: string; + fitToBounds: (layerId: string) => void; + isEditButtonDisabled: boolean; + isReadOnly: boolean; + isUsingSearch: boolean; + layer: ILayer; + removeLayer: (layerId: string) => void; + toggleVisible: (layerId: string) => void; + zoom: number; +} + +interface State { + isPopoverOpen: boolean; + supportsFitToBounds: boolean; +} + +export class TOCEntryActionsPopover extends Component { + private _isMounted: boolean = false; -export class LayerTocActions extends Component { state = { isPopoverOpen: false, supportsFitToBounds: false, @@ -43,6 +66,22 @@ export class LayerTocActions extends Component { })); }; + _cloneLayer() { + this.props.cloneLayer(this.props.layer.getId()); + } + + _fitToBounds() { + this.props.fitToBounds(this.props.layer.getId()); + } + + _removeLayer() { + this.props.fitToBounds(this.props.layer.getId()); + } + + _toggleVisible() { + this.props.toggleVisible(this.props.layer.getId()); + } + _renderPopoverToggleButton() { const { icon, tooltipContent, footnotes } = this.props.layer.getIconAndTooltipContent( this.props.zoom, @@ -108,7 +147,7 @@ export class LayerTocActions extends Component { disabled: !this.state.supportsFitToBounds, onClick: () => { this._closePopover(); - this.props.fitToBounds(); + this._fitToBounds(); }, }, { @@ -121,20 +160,23 @@ export class LayerTocActions extends Component { }), icon: , 'data-test-subj': 'layerVisibilityToggleButton', + toolTipContent: null, onClick: () => { this._closePopover(); - this.props.toggleVisible(); + this._toggleVisible(); }, }, ]; if (!this.props.isReadOnly) { actionItems.push({ + disabled: this.props.isEditButtonDisabled, name: i18n.translate('xpack.maps.layerTocActions.editLayerTitle', { defaultMessage: 'Edit layer', }), icon: , 'data-test-subj': 'editLayerButton', + toolTipContent: null, onClick: () => { this._closePopover(); this.props.editLayer(); @@ -145,10 +187,11 @@ export class LayerTocActions extends Component { defaultMessage: 'Clone layer', }), icon: , + toolTipContent: null, 'data-test-subj': 'cloneLayerButton', onClick: () => { this._closePopover(); - this.props.cloneLayer(); + this._cloneLayer(); }, }); actionItems.push({ @@ -156,10 +199,11 @@ export class LayerTocActions extends Component { defaultMessage: 'Remove layer', }), icon: , + toolTipContent: null, 'data-test-subj': 'removeLayerButton', onClick: () => { this._closePopover(); - this.props.removeLayer(); + this._removeLayer(); }, }); } diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js index fe56523fb2580..c0ce24fef9cd8 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js @@ -8,7 +8,7 @@ import React from 'react'; import classNames from 'classnames'; import { EuiIcon, EuiOverlayMask, EuiButtonIcon, EuiConfirmModal } from '@elastic/eui'; -import { LayerTocActions } from '../../../../../components/layer_toc_actions'; +import { TOCEntryActionsPopover } from './toc_entry_actions_popover'; import { i18n } from '@kbn/i18n'; function escapeLayerName(name) { @@ -124,6 +124,7 @@ export class TOCEntry extends React.Component { return (
- { - fitToBounds(layer.getId()); - }} - zoom={zoom} - toggleVisible={() => { - toggleVisible(layer.getId()); - }} displayName={this.state.displayName} escapedDisplayName={escapeLayerName(this.state.displayName)} - cloneLayer={() => { - cloneLayer(layer.getId()); - }} editLayer={this._openLayerPanelWithCheck} - isReadOnly={isReadOnly} - removeLayer={() => { - removeLayer(layer.getId()); - }} + isEditButtonDisabled={this.props.isEditButtonDisabled} /> {this._renderLayerIcons()} diff --git a/x-pack/plugins/maps/public/layers/layer.tsx b/x-pack/plugins/maps/public/layers/layer.tsx index 13fe447cec3da..dccf413b489f1 100644 --- a/x-pack/plugins/maps/public/layers/layer.tsx +++ b/x-pack/plugins/maps/public/layers/layer.tsx @@ -45,7 +45,7 @@ export interface ILayer { supportsFitToBounds(): Promise; getAttributions(): Promise; getLabel(): string; - getCustomIconAndTooltipContent(): IconAndTooltipContent; + getCustomIconAndTooltipContent(): CustomIconAndTooltipContent; getIconAndTooltipContent(zoomLevel: number, isUsingSearch: boolean): IconAndTooltipContent; renderLegendDetails(): ReactElement | null; showAtZoomLevel(zoom: number): boolean; @@ -87,7 +87,11 @@ export type Footnote = { export type IconAndTooltipContent = { icon?: ReactElement | null; tooltipContent?: string | null; - footnotes?: Footnote[] | null; + footnotes: Footnote[]; +}; +export type CustomIconAndTooltipContent = { + icon: ReactElement | null; + tooltipContent?: string | null; areResultsTrimmed?: boolean; }; @@ -212,7 +216,7 @@ export class AbstractLayer implements ILayer { return this._descriptor.label ? this._descriptor.label : ''; } - getCustomIconAndTooltipContent(): IconAndTooltipContent { + getCustomIconAndTooltipContent(): CustomIconAndTooltipContent { return { icon: , }; diff --git a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts index 633e8c86d8c94..7715541b1c52d 100644 --- a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts +++ b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts @@ -20,6 +20,7 @@ export type RenderWizardArguments = { }; export type LayerWizard = { + checkVisibility?: () => boolean; description: string; icon: string; isIndexingSource?: boolean; @@ -34,5 +35,7 @@ export function registerLayerWizard(layerWizard: LayerWizard) { } export function getLayerWizards(): LayerWizard[] { - return [...registry]; + return registry.filter(layerWizard => { + return layerWizard.checkVisibility ? layerWizard.checkVisibility() : true; + }); } diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index f31e770df2d95..a6e2e7f42657c 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -12,8 +12,13 @@ import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry' import { EMSFileCreateSourceEditor } from './create_source_editor'; // @ts-ignore import { EMSFileSource, sourceTitle } from './ems_file_source'; +// @ts-ignore +import { isEmsEnabled } from '../../../meta'; export const emsBoundariesLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + return isEmsEnabled(); + }, description: i18n.translate('xpack.maps.source.emsFileDescription', { defaultMessage: 'Administrative boundaries from Elastic Maps Service', }), diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index ced33a0bcf84a..fc745edbabee8 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -12,8 +12,13 @@ import { EMSTMSSource, sourceTitle } from './ems_tms_source'; import { VectorTileLayer } from '../../vector_tile_layer'; // @ts-ignore import { TileServiceSelect } from './tile_service_select'; +// @ts-ignore +import { isEmsEnabled } from '../../../meta'; export const emsBaseMapLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + return isEmsEnabled(); + }, description: i18n.translate('xpack.maps.source.emsTileDescription', { defaultMessage: 'Tile map service from Elastic Maps Service', }), diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx index 4321501760faf..a9adec2bda2c8 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx @@ -12,8 +12,14 @@ import { KibanaRegionmapSource, sourceTitle } from './kibana_regionmap_source'; import { VectorLayer } from '../../vector_layer'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; +// @ts-ignore +import { getKibanaRegionList } from '../../../meta'; export const kibanaRegionMapLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + const regions = getKibanaRegionList(); + return regions.length; + }, description: i18n.translate('xpack.maps.source.kbnRegionMapDescription', { defaultMessage: 'Vector data from hosted GeoJSON configured in kibana.yml', }), diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx index aeea2d6084f84..141fabeedd3e5 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx @@ -12,8 +12,14 @@ import { CreateSourceEditor } from './create_source_editor'; // @ts-ignore import { KibanaTilemapSource, sourceTitle } from './kibana_tilemap_source'; import { TileLayer } from '../../tile_layer'; +// @ts-ignore +import { getKibanaTileMap } from '../../../meta'; export const kibanaBasemapLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + const tilemap = getKibanaTileMap(); + return !!tilemap.url; + }, description: i18n.translate('xpack.maps.source.kbnTMSDescription', { defaultMessage: 'Tile map service configured in kibana.yml', }), diff --git a/x-pack/plugins/maps/public/meta.js b/x-pack/plugins/maps/public/meta.js index d4612554cf00b..c3245e8e98db2 100644 --- a/x-pack/plugins/maps/public/meta.js +++ b/x-pack/plugins/maps/public/meta.js @@ -36,12 +36,15 @@ function fetchFunction(...args) { return fetch(...args); } +export function isEmsEnabled() { + return getInjectedVarFunc()('isEmsEnabled', true); +} + let emsClient = null; let latestLicenseId = null; export function getEMSClient() { if (!emsClient) { - const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true); - if (isEmsEnabled) { + if (isEmsEnabled()) { const proxyElasticMapsServiceInMaps = getInjectedVarFunc()( 'proxyElasticMapsServiceInMaps', false @@ -86,7 +89,7 @@ export function getEMSClient() { } export function getGlyphUrl() { - if (!getInjectedVarFunc()('isEmsEnabled', true)) { + if (!isEmsEnabled()) { return ''; } return getInjectedVarFunc()('proxyElasticMapsServiceInMaps', false) diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts index 4d0f652af982a..bc881d06f62ce 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts @@ -22,4 +22,6 @@ export function getMapSettings(state: MapStoreState): MapSettings; export function hasMapSettingsChanges(state: MapStoreState): boolean; +export function isUsingSearch(state: MapStoreState): boolean; + export function getSpatialFiltersLayer(state: MapStoreState): IVectorLayer; diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 2a449c95faa5b..572217ce16eee 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -7,6 +7,7 @@ import { KibanaRequest } from 'kibana/server'; export const userMlCapabilities = { + canAccessML: false, // Anomaly Detection canGetJobs: false, canGetDatafeeds: false, @@ -14,10 +15,12 @@ export const userMlCapabilities = { canGetCalendars: false, // File Data Visualizer canFindFileStructure: false, - // Filters - canGetFilters: false, // Data Frame Analytics canGetDataFrameAnalytics: false, + // Annotations + canGetAnnotations: false, + canCreateAnnotation: false, + canDeleteAnnotation: false, }; export const adminMlCapabilities = { @@ -26,11 +29,15 @@ export const adminMlCapabilities = { canDeleteJob: false, canOpenJob: false, canCloseJob: false, + canUpdateJob: false, canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, canStartStopDatafeed: false, - canUpdateJob: false, canUpdateDatafeed: false, canPreviewDatafeed: false, + // Filters + canGetFilters: false, // Calendars canCreateCalendar: false, canDeleteCalendar: false, @@ -38,8 +45,8 @@ export const adminMlCapabilities = { canCreateFilter: false, canDeleteFilter: false, // Data Frame Analytics - canDeleteDataFrameAnalytics: false, canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, canStartStopDataFrameAnalytics: false, }; @@ -47,7 +54,9 @@ export type UserMlCapabilities = typeof userMlCapabilities; export type AdminMlCapabilities = typeof adminMlCapabilities; export type MlCapabilities = UserMlCapabilities & AdminMlCapabilities; -export const basicLicenseMlCapabilities = ['canFindFileStructure'] as Array; +export const basicLicenseMlCapabilities = ['canAccessML', 'canFindFileStructure'] as Array< + keyof MlCapabilities +>; export function getDefaultCapabilities(): MlCapabilities { return { @@ -56,6 +65,23 @@ export function getDefaultCapabilities(): MlCapabilities { }; } +export function getPluginPrivileges() { + const userMlCapabilitiesKeys = Object.keys(userMlCapabilities); + const adminMlCapabilitiesKeys = Object.keys(adminMlCapabilities); + const allMlCapabilities = [...adminMlCapabilitiesKeys, ...userMlCapabilitiesKeys]; + + return { + user: { + ui: userMlCapabilitiesKeys, + api: userMlCapabilitiesKeys.map(k => `ml:${k}`), + }, + admin: { + ui: allMlCapabilities, + api: allMlCapabilities.map(k => `ml:${k}`), + }, + }; +} + export interface MlCapabilitiesResponse { capabilities: MlCapabilities; upgradeInProgress: boolean; diff --git a/x-pack/plugins/ml/common/util/job_utils.d.ts b/x-pack/plugins/ml/common/util/job_utils.d.ts index bfad422e0ab48..4528fbfbb774d 100644 --- a/x-pack/plugins/ml/common/util/job_utils.d.ts +++ b/x-pack/plugins/ml/common/util/job_utils.d.ts @@ -52,3 +52,5 @@ export function getLatestDataOrBucketTimestamp( ): number; export function prefixDatafeedId(datafeedId: string, prefix: string): string; + +export function splitIndexPatternNames(indexPatternName: string): string[]; diff --git a/x-pack/plugins/ml/common/util/job_utils.js b/x-pack/plugins/ml/common/util/job_utils.js index de0aa4b886629..8fe5733ce67bd 100644 --- a/x-pack/plugins/ml/common/util/job_utils.js +++ b/x-pack/plugins/ml/common/util/job_utils.js @@ -588,3 +588,9 @@ export function processCreatedBy(customSettings) { delete customSettings.created_by; } } + +export function splitIndexPatternNames(indexPatternName) { + return indexPatternName.includes(',') + ? indexPatternName.split(',').map(i => i.trim()) + : [indexPatternName]; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index 8c65af1d92959..cc75ddbe08cfb 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -18,6 +18,7 @@ import { } from '../../hooks/use_create_analytics_form'; import { State } from '../../hooks/use_create_analytics_form/state'; import { DataFrameAnalyticsListRow } from './common'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; interface PropDefinition { /** @@ -322,6 +323,8 @@ interface CloneActionProps { * to support EuiContext with a valid DOM structure without nested buttons. */ export const CloneAction: FC = ({ createAnalyticsForm, item }) => { + const canCreateDataFrameAnalytics: boolean = checkPermission('canCreateDataFrameAnalytics'); + const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.cloneJobButtonLabel', { defaultMessage: 'Clone job', }); @@ -338,6 +341,7 @@ export const CloneAction: FC = ({ createAnalyticsForm, item }) iconType="copy" onClick={onClick} aria-label={buttonText} + disabled={canCreateDataFrameAnalytics === false} > {buttonText} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts index 306fd82dc8758..9dda8eec206e4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts @@ -7,6 +7,7 @@ import { IndexPatternTitle } from '../../../../../../../common/types/kibana'; import { Field, Aggregation, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; import { Job, Datafeed, Detector } from '../../../../../../../common/types/anomaly_detection_jobs'; +import { splitIndexPatternNames } from '../../../../../../../common/util/job_utils'; export function createEmptyJob(): Job { return { @@ -28,7 +29,7 @@ export function createEmptyDatafeed(indexPatternTitle: IndexPatternTitle): Dataf return { datafeed_id: '', job_id: '', - indices: [indexPatternTitle], + indices: splitIndexPatternNames(indexPatternTitle), query: {}, }; } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts index 50a84eb3d11cb..69df2773f9f8d 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts @@ -50,7 +50,7 @@ function getWizardUrlFromCloningJob(job: CombinedJob) { break; } - const indexPatternId = getIndexPatternIdFromName(job.datafeed_config.indices[0]); + const indexPatternId = getIndexPatternIdFromName(job.datafeed_config.indices.join()); return `jobs/new_job/${page}?index=${indexPatternId}&_g=()`; } diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx index b2eda12abc578..c379cd702daee 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx @@ -82,14 +82,14 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => { title={

{i18n.translate('xpack.ml.overview.analyticsList.createFirstJobMessage', { - defaultMessage: 'Create your first analytics job', + defaultMessage: 'Create your first data frame analytics job', })}

} body={

{i18n.translate('xpack.ml.overview.analyticsList.emptyPromptText', { - defaultMessage: `Data frame analytics enable you to perform different analyses of your data and annotate it with the results. The analytics job stores the annotated data, as well as a copy of the source data, in a new index.`, + defaultMessage: `Data frame analytics enable you to perform different analyses of your data and annotates it with the results. The job puts the annotated data and a copy of the source data in a new index.`, })}

} diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx index 5f5c3f7c28670..dac39b1a2071d 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx @@ -172,7 +172,7 @@ export const AnomalyDetectionPanel: FC = ({ jobCreationDisabled }) => {

{i18n.translate('xpack.ml.overview.anomalyDetection.emptyPromptText', { - defaultMessage: `Machine learning makes it easy to detect anomalies in time series data stored in Elasticsearch. Track one metric from a single machine or hundreds of metrics across thousands of machines. Start automatically spotting the anomalies hiding in your data and resolve issues faster.`, + defaultMessage: `Anomaly detection enables you to find unusual behavior in time series data. Start automatically spotting the anomalies hiding in your data and resolve issues faster.`, })}

diff --git a/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx b/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx index 219c195bab111..3e4e9cfbd2b66 100644 --- a/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx @@ -11,7 +11,6 @@ import { useMlKibana } from '../../contexts/kibana'; const createJobLink = '#/jobs/new_job/step/index_or_search'; const feedbackLink = 'https://www.elastic.co/community/'; -const whatIsMachineLearningLink = 'https://www.elastic.co/what-is/elasticsearch-machine-learning'; interface Props { createAnomalyDetectionJobDisabled: boolean; @@ -60,7 +59,7 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }

@@ -79,14 +78,6 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled } /> ), - whatIsMachineLearning: ( - - - - ), }} />

@@ -96,7 +87,7 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }

diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts index 5093801d2d184..746c9da47d0ad 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -36,7 +36,7 @@ describe('check_capabilities', () => { ); const { capabilities } = await getCapabilities(); const count = Object.keys(capabilities).length; - expect(count).toBe(22); + expect(count).toBe(28); done(); }); }); @@ -49,28 +49,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(true); + expect(capabilities.canDeleteAnnotation).toBe(true); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -84,28 +98,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(true); + expect(capabilities.canDeleteAnnotation).toBe(true); + expect(capabilities.canCreateJob).toBe(true); expect(capabilities.canDeleteJob).toBe(true); expect(capabilities.canOpenJob).toBe(true); expect(capabilities.canCloseJob).toBe(true); expect(capabilities.canForecastJob).toBe(true); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(true); expect(capabilities.canUpdateJob).toBe(true); + expect(capabilities.canCreateDatafeed).toBe(true); + expect(capabilities.canDeleteDatafeed).toBe(true); expect(capabilities.canUpdateDatafeed).toBe(true); expect(capabilities.canPreviewDatafeed).toBe(true); - expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateCalendar).toBe(true); expect(capabilities.canDeleteCalendar).toBe(true); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(true); expect(capabilities.canDeleteFilter).toBe(true); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(true); expect(capabilities.canCreateDataFrameAnalytics).toBe(true); expect(capabilities.canStartStopDataFrameAnalytics).toBe(true); @@ -119,28 +147,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(true); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); + expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -154,28 +196,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(true); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -189,28 +245,42 @@ describe('check_capabilities', () => { mlLicense, mlIsNotEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(false); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(false); expect(capabilities.canGetJobs).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(false); + expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canFindFileStructure).toBe(false); + expect(capabilities.canGetDataFrameAnalytics).toBe(false); + expect(capabilities.canGetAnnotations).toBe(false); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -225,28 +295,43 @@ describe('check_capabilities', () => { mlLicenseBasic, mlIsNotEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(false); + expect(isPlatinumOrTrialLicense).toBe(false); + + expect(capabilities.canAccessML).toBe(false); expect(capabilities.canGetJobs).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(false); + expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canFindFileStructure).toBe(false); + expect(capabilities.canGetDataFrameAnalytics).toBe(false); + expect(capabilities.canGetAnnotations).toBe(false); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts index a2ad83c5522de..d955cf981faca 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts @@ -44,4 +44,6 @@ function disableAdminPrivileges(capabilities: MlCapabilities) { Object.keys(adminMlCapabilities).forEach(k => { capabilities[k as keyof MlCapabilities] = false; }); + capabilities.canCreateAnnotation = false; + capabilities.canDeleteAnnotation = false; } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 84f81a30f36b8..40b2a524151b3 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -29,7 +29,11 @@ import { JobSpecificOverride, isGeneralJobOverride, } from '../../../common/types/modules'; -import { getLatestDataOrBucketTimestamp, prefixDatafeedId } from '../../../common/util/job_utils'; +import { + getLatestDataOrBucketTimestamp, + prefixDatafeedId, + splitIndexPatternNames, +} from '../../../common/util/job_utils'; import { mlLog } from '../../client/log'; import { calculateModelMemoryLimitProvider } from '../calculate_model_memory_limit'; import { fieldsServiceProvider } from '../fields_service'; @@ -828,9 +832,7 @@ export class DataRecognizer { updateDatafeedIndices(moduleConfig: Module) { // if the supplied index pattern contains a comma, split into multiple indices and // add each one to the datafeed - const indexPatternNames = this.indexPatternName.includes(',') - ? this.indexPatternName.split(',').map(i => i.trim()) - : [this.indexPatternName]; + const indexPatternNames = splitIndexPatternNames(this.indexPatternName); moduleConfig.datafeeds.forEach(df => { const newIndices: string[] = []; diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 64f8eb4b0acd3..969b74194148b 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -45,7 +45,7 @@ import { systemRoutes } from './routes/system'; import { MlLicense } from '../common/license'; import { MlServerLicense } from './lib/license'; import { createSharedServices, SharedServices } from './shared_services'; -import { userMlCapabilities, adminMlCapabilities } from '../common/types/capabilities'; +import { getPluginPrivileges } from '../common/types/capabilities'; import { setupCapabilitiesSwitcher } from './lib/capabilities'; import { registerKibanaSettings } from './lib/register_settings'; @@ -75,8 +75,7 @@ export class MlServerPlugin implements Plugin { try { @@ -86,6 +89,9 @@ export function annotationRoutes( validate: { body: indexAnnotationSchema, }, + options: { + tags: ['access:ml:canCreateAnnotation'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -130,6 +136,9 @@ export function annotationRoutes( validate: { params: deleteAnnotationSchema, }, + options: { + tags: ['access:ml:canDeleteAnnotation'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index ca63d69f403f6..63cd5498231af 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -37,6 +37,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -65,6 +68,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -93,6 +99,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors/_stats', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -121,6 +130,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -154,6 +166,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: schema.object(anomalyDetectionJobSchema), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -188,6 +203,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: anomalyDetectionUpdateJobSchema, }, + options: { + tags: ['access:ml:canUpdateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -220,6 +238,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canOpenJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -251,6 +272,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canCloseJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -286,6 +310,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canDeleteJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -319,6 +346,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: schema.any(), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -351,6 +381,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: forecastAnomalyDetector, }, + options: { + tags: ['access:ml:canForecastJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -389,6 +422,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: getRecordsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -425,6 +461,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: getBucketParamsSchema, body: getBucketsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -462,6 +501,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: getOverallBucketsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -496,6 +538,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: getCategoriesSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/calendars.ts b/x-pack/plugins/ml/server/routes/calendars.ts index a17601f74ae93..9c80651a13999 100644 --- a/x-pack/plugins/ml/server/routes/calendars.ts +++ b/x-pack/plugins/ml/server/routes/calendars.ts @@ -52,6 +52,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/calendars', validate: false, + options: { + tags: ['access:ml:canGetCalendars'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -81,6 +84,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { validate: { params: calendarIdsSchema, }, + options: { + tags: ['access:ml:canGetCalendars'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { let returnValue; @@ -117,6 +123,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { validate: { body: calendarSchema, }, + options: { + tags: ['access:ml:canCreateCalendar'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -149,6 +158,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { params: calendarIdSchema, body: calendarSchema, }, + options: { + tags: ['access:ml:canCreateCalendar'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -180,6 +192,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { validate: { params: calendarIdSchema, }, + options: { + tags: ['access:ml:canDeleteCalendar'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index dd9e0ea66aa9d..32cb2b343f876 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -33,6 +33,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat { path: '/api/ml/data_frame/analytics', validate: false, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -61,6 +64,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -88,6 +94,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat { path: '/api/ml/data_frame/analytics/_stats', validate: false, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -118,6 +127,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -155,6 +167,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat params: analyticsIdSchema, body: dataAnalyticsJobConfigSchema, }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -190,6 +205,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { body: dataAnalyticsEvaluateSchema, }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -224,6 +242,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { body: dataAnalyticsExplainSchema, }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -257,6 +278,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canDeleteDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -291,6 +315,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canStartStopDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -324,6 +351,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat params: analyticsIdSchema, query: stopsDataFrameAnalyticsJobQuerySchema, }, + options: { + tags: ['access:ml:canStartStopDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -364,6 +394,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts index 20029fbd8d1a6..04008a896a1a2 100644 --- a/x-pack/plugins/ml/server/routes/data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts @@ -88,6 +88,9 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) params: indexPatternTitleSchema, body: dataVisualizerFieldStatsSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -150,6 +153,9 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) params: indexPatternTitleSchema, body: dataVisualizerOverallStatsSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/datafeeds.ts b/x-pack/plugins/ml/server/routes/datafeeds.ts index ec667e1d305f5..1fa1d408372da 100644 --- a/x-pack/plugins/ml/server/routes/datafeeds.ts +++ b/x-pack/plugins/ml/server/routes/datafeeds.ts @@ -28,6 +28,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/datafeeds', validate: false, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -57,6 +60,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -83,6 +89,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/datafeeds/_stats', validate: false, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -112,6 +121,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -146,6 +158,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, body: datafeedConfigSchema, }, + options: { + tags: ['access:ml:canCreateDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -181,6 +196,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, body: datafeedConfigSchema, }, + options: { + tags: ['access:ml:canUpdateDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -216,6 +234,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, query: deleteDatafeedQuerySchema, }, + options: { + tags: ['access:ml:canDeleteDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -255,6 +276,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, body: startDatafeedSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -291,6 +315,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -324,6 +351,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canPreviewDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts index 577e8e0161342..b0f13df294145 100644 --- a/x-pack/plugins/ml/server/routes/fields_service.ts +++ b/x-pack/plugins/ml/server/routes/fields_service.ts @@ -46,8 +46,10 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { validate: { body: getCardinalityOfFieldsSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, - mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await getCardinalityOfFields(context, request.body); @@ -79,6 +81,9 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { validate: { body: getTimeFieldRangeSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts index 3f3fc3f547b6a..0f389f9505943 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -71,6 +71,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat accepts: ['text/*', 'application/json'], maxBytes: MAX_FILE_SIZE_BYTES, }, + tags: ['access:ml:canFindFileStructure'], }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { @@ -105,6 +106,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat accepts: ['application/json'], maxBytes: MAX_FILE_SIZE_BYTES, }, + tags: ['access:ml:canFindFileStructure'], }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/filters.ts b/x-pack/plugins/ml/server/routes/filters.ts index 738c25070358d..d5287c349a8fc 100644 --- a/x-pack/plugins/ml/server/routes/filters.ts +++ b/x-pack/plugins/ml/server/routes/filters.ts @@ -57,6 +57,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters', validate: false, + options: { + tags: ['access:ml:canGetFilters'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -89,6 +92,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: filterIdSchema, }, + options: { + tags: ['access:ml:canGetFilters'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -120,6 +126,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: createFilterSchema, }, + options: { + tags: ['access:ml:canCreateFilter'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -155,6 +164,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { params: filterIdSchema, body: updateFilterSchema, }, + options: { + tags: ['access:ml:canCreateFilter'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -186,6 +198,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: filterIdSchema, }, + options: { + tags: ['access:ml:canDeleteFilter'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -216,6 +231,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters/_stats', validate: false, + options: { + tags: ['access:ml:canGetFilters'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/indices.ts b/x-pack/plugins/ml/server/routes/indices.ts index e434936beba63..fb3ef7fc41c76 100644 --- a/x-pack/plugins/ml/server/routes/indices.ts +++ b/x-pack/plugins/ml/server/routes/indices.ts @@ -27,6 +27,9 @@ export function indicesRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: indicesSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index 1fe5a7af95d4f..5acc89e7d13be 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -33,6 +33,9 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio params: jobAuditMessagesJobIdSchema, query: jobAuditMessagesQuerySchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -67,6 +70,9 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio validate: { query: jobAuditMessagesQuerySchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 149ca2591fd76..05c44e1da9757 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; import { schema } from '@kbn/config-schema'; -import { KibanaRequest } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization, JobServiceRouteDeps } from '../types'; +import { RouteInitialization } from '../types'; import { categorizationFieldExamplesSchema, chartSchema, @@ -27,19 +25,7 @@ import { categorizationExamplesProvider } from '../models/job_service/new_job'; /** * Routes for job service */ -export function jobServiceRoutes( - { router, mlLicense }: RouteInitialization, - { resolveMlCapabilities }: JobServiceRouteDeps -) { - async function hasPermissionToCreateJobs(request: KibanaRequest) { - const mlCapabilities = await resolveMlCapabilities(request); - if (mlCapabilities === null) { - throw new Error('resolveMlCapabilities is not defined'); - } - - return mlCapabilities.canCreateJob; - } - +export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup JobService * @@ -55,6 +41,9 @@ export function jobServiceRoutes( validate: { body: forceStartDatafeedSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -86,6 +75,9 @@ export function jobServiceRoutes( validate: { body: datafeedIdsSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -117,6 +109,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canDeleteJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -148,6 +143,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canCloseJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -184,6 +182,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -215,6 +216,9 @@ export function jobServiceRoutes( validate: { body: schema.object(jobsWithTimerangeSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -245,6 +249,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -272,6 +279,9 @@ export function jobServiceRoutes( { path: '/api/ml/jobs/groups', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -302,6 +312,9 @@ export function jobServiceRoutes( validate: { body: schema.object(updateGroupsSchema), }, + options: { + tags: ['access:ml:canUpdateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -329,6 +342,9 @@ export function jobServiceRoutes( { path: '/api/ml/jobs/deleting_jobs_tasks', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -359,6 +375,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -389,6 +408,9 @@ export function jobServiceRoutes( params: schema.object({ indexPattern: schema.string() }), query: schema.maybe(schema.object({ rollup: schema.maybe(schema.string()) })), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -422,6 +444,9 @@ export function jobServiceRoutes( validate: { body: schema.object(chartSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -474,6 +499,9 @@ export function jobServiceRoutes( validate: { body: schema.object(chartSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -522,6 +550,9 @@ export function jobServiceRoutes( { path: '/api/ml/jobs/all_jobs_and_group_ids', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -552,6 +583,9 @@ export function jobServiceRoutes( validate: { body: schema.object(lookBackProgressSchema), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -583,17 +617,12 @@ export function jobServiceRoutes( validate: { body: schema.object(categorizationFieldExamplesSchema), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { - // due to the use of the _analyze endpoint which is called by the kibana user, - // basic job creation privileges are required to use this endpoint - if ((await hasPermissionToCreateJobs(request)) === false) { - throw Boom.forbidden( - 'Insufficient privileges, the machine_learning_admin role is required.' - ); - } - const { validateCategoryExamples } = categorizationExamplesProvider( context.ml!.mlClient.callAsCurrentUser, context.ml!.mlClient.callAsInternalUser @@ -644,6 +673,9 @@ export function jobServiceRoutes( validate: { body: schema.object(topCategoriesSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index dd2bd9deadf43..632166d6d5fb8 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -57,6 +57,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: estimateBucketSpanSchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -106,6 +109,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: modelMemoryLimitSchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -135,6 +141,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: validateCardinalitySchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -167,6 +176,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: validateJobSchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts index 2891144fc4574..622ae66ede426 100644 --- a/x-pack/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -97,6 +97,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { indexPatternTitle: schema.string(), }), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -127,6 +130,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { ...getModuleIdParamSchema(true), }), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -161,6 +167,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { params: schema.object(getModuleIdParamSchema()), body: setupModuleBodySchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -218,6 +227,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { validate: { params: schema.object(getModuleIdParamSchema()), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/notification_settings.ts b/x-pack/plugins/ml/server/routes/notification_settings.ts index 59458b1e486db..e4a9abb0784be 100644 --- a/x-pack/plugins/ml/server/routes/notification_settings.ts +++ b/x-pack/plugins/ml/server/routes/notification_settings.ts @@ -22,6 +22,9 @@ export function notificationRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/notification_settings', validate: false, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index 89c267340fe52..94ca0827ccfa5 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -88,6 +88,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: anomaliesTableDataSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -117,6 +120,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: categoryDefinitionSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -146,6 +152,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: maxAnomalyScoreSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -175,6 +184,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: categoryExamplesSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -204,6 +216,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: partitionFieldValuesSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts index d5fe45728c56c..7ae7dd8eef065 100644 --- a/x-pack/plugins/ml/server/routes/system.ts +++ b/x-pack/plugins/ml/server/routes/system.ts @@ -54,6 +54,9 @@ export function systemRoutes( validate: { body: schema.maybe(schema.any()), }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -110,6 +113,9 @@ export function systemRoutes( { path: '/api/ml/ml_capabilities', validate: false, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -150,7 +156,11 @@ export function systemRoutes( { path: '/api/ml/ml_node_count', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, + mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { // check for basic license first for consistency with other @@ -201,6 +211,9 @@ export function systemRoutes( { path: '/api/ml/info', validate: false, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -229,6 +242,9 @@ export function systemRoutes( validate: { body: schema.maybe(schema.any()), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts index d4cd61a7fa4f7..678e81d3526ac 100644 --- a/x-pack/plugins/ml/server/types.ts +++ b/x-pack/plugins/ml/server/types.ts @@ -30,10 +30,6 @@ export interface SystemRouteDeps { resolveMlCapabilities: ResolveMlCapabilities; } -export interface JobServiceRouteDeps { - resolveMlCapabilities: ResolveMlCapabilities; -} - export interface PluginsSetup { cloud: CloudSetup; features: FeaturesPluginSetup; diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index edd6142455dfb..eeed7b4d5acf6 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -245,7 +245,7 @@ export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION, ALERT_TYPE_CLUSTER_ST /** * Matches the id for the built-in in email action type - * See x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts + * See x-pack/plugins/actions/server/builtin_action_types/email.ts */ export const ALERT_ACTION_TYPE_EMAIL = '.email'; diff --git a/x-pack/plugins/security/public/account_management/account_management_app.ts b/x-pack/plugins/security/public/account_management/account_management_app.ts index cd3ef34858b19..41567a04fe030 100644 --- a/x-pack/plugins/security/public/account_management/account_management_app.ts +++ b/x-pack/plugins/security/public/account_management/account_management_app.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { StartServicesAccessor, ApplicationSetup, AppMountParameters } from 'src/core/public'; import { AuthenticationServiceSetup } from '../authentication'; -import { UserAPIClient } from '../management'; interface CreateDeps { application: ApplicationSetup; @@ -28,9 +27,14 @@ export const accountManagementApp = Object.freeze({ navLinkStatus: 3, appRoute: '/security/account', async mount({ element }: AppMountParameters) { - const [[coreStart], { renderAccountManagementPage }] = await Promise.all([ + const [ + [coreStart], + { renderAccountManagementPage }, + { UserAPIClient }, + ] = await Promise.all([ getStartServices(), import('./account_management_page'), + import('../management'), ]); coreStart.chrome.setBreadcrumbs([{ text: title }]); diff --git a/x-pack/plugins/security/public/authentication/access_agreement/_index.scss b/x-pack/plugins/security/public/authentication/access_agreement/_index.scss deleted file mode 100644 index dbab8347b096f..0000000000000 --- a/x-pack/plugins/security/public/authentication/access_agreement/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './access_agreement_page'; diff --git a/x-pack/plugins/security/public/authentication/access_agreement/_access_agreement_page.scss b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.scss similarity index 100% rename from x-pack/plugins/security/public/authentication/access_agreement/_access_agreement_page.scss rename to x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.scss diff --git a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.tsx b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.tsx index 0315e229c678b..a34dcb18d2b9c 100644 --- a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.tsx +++ b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import './_index.scss'; +import './access_agreement_page.scss'; import React, { FormEvent, MouseEvent, useCallback, useEffect, useState } from 'react'; import ReactDOM from 'react-dom'; diff --git a/x-pack/plugins/security/public/authentication/components/authentication_state_page/_authentication_state_page.scss b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.scss similarity index 100% rename from x-pack/plugins/security/public/authentication/components/authentication_state_page/_authentication_state_page.scss rename to x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.scss diff --git a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx index 7567f455bcca6..35be650d127fb 100644 --- a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx +++ b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import './_authentication_state_page.scss'; +import './authentication_state_page.scss'; import { EuiIcon, EuiSpacer, EuiTitle } from '@elastic/eui'; import React from 'react'; diff --git a/x-pack/plugins/security/public/authentication/login/_login_page.scss b/x-pack/plugins/security/public/authentication/login/login_page.scss similarity index 100% rename from x-pack/plugins/security/public/authentication/login/_login_page.scss rename to x-pack/plugins/security/public/authentication/login/login_page.scss diff --git a/x-pack/plugins/security/public/authentication/login/login_page.tsx b/x-pack/plugins/security/public/authentication/login/login_page.tsx index b7ac70f2aaf89..d24a301ed24ec 100644 --- a/x-pack/plugins/security/public/authentication/login/login_page.tsx +++ b/x-pack/plugins/security/public/authentication/login/login_page.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import './_login_page.scss'; +import './login_page.scss'; import React, { Component } from 'react'; import ReactDOM from 'react-dom'; diff --git a/x-pack/plugins/security/public/index.scss b/x-pack/plugins/security/public/index.scss deleted file mode 100644 index 1bdb8cc178fdf..0000000000000 --- a/x-pack/plugins/security/public/index.scss +++ /dev/null @@ -1,4 +0,0 @@ -$secFormWidth: 460px; - -// Management styles -@import './management/index'; diff --git a/x-pack/plugins/security/public/index.ts b/x-pack/plugins/security/public/index.ts index fc4e158652a0a..8016c94224060 100644 --- a/x-pack/plugins/security/public/index.ts +++ b/x-pack/plugins/security/public/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './index.scss'; import { PluginInitializer, PluginInitializerContext } from 'src/core/public'; import { SecurityPlugin, diff --git a/x-pack/plugins/security/public/management/_index.scss b/x-pack/plugins/security/public/management/_index.scss deleted file mode 100644 index 5d419b5323079..0000000000000 --- a/x-pack/plugins/security/public/management/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './roles/index'; -@import './users/index'; -@import './role_mappings/index'; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx index 272fc9cfc2fe6..b9ec5b35b3f9d 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx @@ -10,8 +10,6 @@ import { i18n } from '@kbn/i18n'; import { StartServicesAccessor } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { PluginStartDependencies } from '../../plugin'; -import { APIKeysGridPage } from './api_keys_grid'; -import { APIKeysAPIClient } from './api_keys_api_client'; import { DocumentationLinksService } from './documentation_links'; interface CreateParams { @@ -28,7 +26,6 @@ export const apiKeysManagementApp = Object.freeze({ defaultMessage: 'API Keys', }), async mount({ basePath, element, setBreadcrumbs }) { - const [{ docLinks, http, notifications, i18n: i18nStart }] = await getStartServices(); setBreadcrumbs([ { text: i18n.translate('xpack.security.apiKeys.breadcrumb', { @@ -38,6 +35,16 @@ export const apiKeysManagementApp = Object.freeze({ }, ]); + const [ + [{ docLinks, http, notifications, i18n: i18nStart }], + { APIKeysGridPage }, + { APIKeysAPIClient }, + ] = await Promise.all([ + getStartServices(), + import('./api_keys_grid'), + import('./api_keys_api_client'), + ]); + render( ; @@ -31,7 +27,6 @@ export const roleMappingsManagementApp = Object.freeze({ defaultMessage: 'Role Mappings', }), async mount({ basePath, element, setBreadcrumbs }) { - const [{ docLinks, http, notifications, i18n: i18nStart }] = await getStartServices(); const roleMappingsBreadcrumbs = [ { text: i18n.translate('xpack.security.roleMapping.breadcrumb', { @@ -41,6 +36,20 @@ export const roleMappingsManagementApp = Object.freeze({ }, ]; + const [ + [{ docLinks, http, notifications, i18n: i18nStart }], + { RoleMappingsGridPage }, + { EditRoleMappingPage }, + { RoleMappingsAPIClient }, + { RolesAPIClient }, + ] = await Promise.all([ + getStartServices(), + import('./role_mappings_grid'), + import('./edit_role_mapping'), + import('./role_mappings_api_client'), + import('../roles'), + ]); + const roleMappingsAPIClient = new RoleMappingsAPIClient(http); const dockLinksService = new DocumentationLinksService(docLinks); const RoleMappingsGridPageWithBreadcrumbs = () => { diff --git a/x-pack/plugins/security/public/management/roles/_index.scss b/x-pack/plugins/security/public/management/roles/_index.scss deleted file mode 100644 index 5256c79f01f10..0000000000000 --- a/x-pack/plugins/security/public/management/roles/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './edit_role/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/_index.scss deleted file mode 100644 index 0153b1734ceba..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './collapsible_panel/index'; -@import './spaces_popover_list/index'; -@import './privileges/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss deleted file mode 100644 index c0f4f8ab9a870..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './collapsible_panel'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_collapsible_panel.scss b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.scss similarity index 100% rename from x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_collapsible_panel.scss rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx index 01af7cb4509f6..eb1417600e19b 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './collapsible_panel.scss'; + import { EuiFlexGroup, EuiFlexItem, diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss deleted file mode 100644 index a1a9d038065e6..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './privilege_feature_icon'; -@import './kibana/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss deleted file mode 100644 index 19547c0e1953e..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './feature_table/index'; -@import './space_aware_privilege_section/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss deleted file mode 100644 index 6a96553742819..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './change_all_privileges'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_change_all_privileges.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.scss similarity index 100% rename from x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_change_all_privileges.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx index 2083778e53998..5d7b13acf79da 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import './change_all_privileges.scss'; + import { EuiContextMenuItem, EuiContextMenuPanel, EuiLink, EuiPopover } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.scss similarity index 100% rename from x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.tsx index 9e4a3a8a99b56..77445952f3d69 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './feature_table_cell.scss'; + import React from 'react'; import { EuiText, EuiIconTip, EuiIcon, IconType } from '@elastic/eui'; import { SecuredFeature } from '../../../../model'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss deleted file mode 100644 index 3f40f21e102a1..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './privilege_matrix'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss deleted file mode 100644 index 8f47727fdf8d6..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 1. Allow table to scroll both directions - */ - -.secPrivilegeMatrix__modal, -.secPrivilegeMatrix__modal .euiModal__flex { - overflow: hidden; /* 1 */ -} - -.secPrivilegeMatrix__row--isBasePrivilege, -.secPrivilegeMatrix__cell--isGlobalPrivilege, -.secPrivilegeTable__row--isGlobalSpace { - background-color: $euiColorLightestShade; -} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.scss new file mode 100644 index 0000000000000..8e2a3b0512afb --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.scss @@ -0,0 +1,3 @@ +.secPrivilegeTable__row--isGlobalSpace { + background-color: $euiColorLightestShade; +} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx index ccb5398a11b23..30a275876fdc7 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import './privilege_space_table.scss'; + import { EuiBadge, EuiBadgeProps, diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss deleted file mode 100644 index b40a32cb8df96..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './spaces_popover_list'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_spaces_popover_list.scss b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.scss similarity index 100% rename from x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_spaces_popover_list.scss rename to x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx index 92e42ec811afc..63ee311f3155e 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './spaces_popover_list.scss'; + import { EuiButtonEmpty, EuiContextMenuItem, diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx index e1a10fdc2b8c3..9aaa3b47f3b19 100644 --- a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx @@ -12,13 +12,7 @@ import { StartServicesAccessor, FatalErrorsSetup } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { SecurityLicense } from '../../../common/licensing'; import { PluginStartDependencies } from '../../plugin'; -import { UserAPIClient } from '../users'; -import { RolesAPIClient } from './roles_api_client'; -import { RolesGridPage } from './roles_grid'; -import { EditRolePage } from './edit_role'; import { DocumentationLinksService } from './documentation_links'; -import { IndicesAPIClient } from './indices_api_client'; -import { PrivilegesAPIClient } from './privileges_api_client'; interface CreateParams { fatalErrors: FatalErrorsSetup; @@ -34,11 +28,6 @@ export const rolesManagementApp = Object.freeze({ order: 20, title: i18n.translate('xpack.security.management.rolesTitle', { defaultMessage: 'Roles' }), async mount({ basePath, element, setBreadcrumbs }) { - const [ - { application, docLinks, http, i18n: i18nStart, injectedMetadata, notifications }, - { data, features }, - ] = await getStartServices(); - const rolesBreadcrumbs = [ { text: i18n.translate('xpack.security.roles.breadcrumb', { defaultMessage: 'Roles' }), @@ -46,6 +35,27 @@ export const rolesManagementApp = Object.freeze({ }, ]; + const [ + [ + { application, docLinks, http, i18n: i18nStart, injectedMetadata, notifications }, + { data, features }, + ], + { RolesGridPage }, + { EditRolePage }, + { RolesAPIClient }, + { IndicesAPIClient }, + { PrivilegesAPIClient }, + { UserAPIClient }, + ] = await Promise.all([ + getStartServices(), + import('./roles_grid'), + import('./edit_role'), + import('./roles_api_client'), + import('./indices_api_client'), + import('./privileges_api_client'), + import('../users'), + ]); + const rolesAPIClient = new RolesAPIClient(http); const RolesGridPageWithBreadcrumbs = () => { setBreadcrumbs(rolesBreadcrumbs); diff --git a/x-pack/plugins/security/public/management/users/_index.scss b/x-pack/plugins/security/public/management/users/_index.scss deleted file mode 100644 index 35df0c1b96583..0000000000000 --- a/x-pack/plugins/security/public/management/users/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './edit_user/index'; diff --git a/x-pack/plugins/security/public/management/users/edit_user/_index.scss b/x-pack/plugins/security/public/management/users/edit_user/_index.scss deleted file mode 100644 index 734ba7882ba72..0000000000000 --- a/x-pack/plugins/security/public/management/users/edit_user/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './edit_user_page'; diff --git a/x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss similarity index 76% rename from x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss rename to x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss index 7b24b74aceba0..727fac4782752 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss @@ -1,5 +1,5 @@ .secUsersEditPage__content { - max-width: $secFormWidth; + max-width: 460px; margin-left: auto; margin-right: auto; flex-grow: 0; diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index 6417ce81b647d..1c8130029bb50 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import './edit_user_page.scss'; + import { get } from 'lodash'; import React, { Component, Fragment, ChangeEvent } from 'react'; import { diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx index 82a2b8d2a98ad..9d337c1508ad4 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -12,10 +12,6 @@ import { StartServicesAccessor } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { AuthenticationServiceSetup } from '../../authentication'; import { PluginStartDependencies } from '../../plugin'; -import { RolesAPIClient } from '../roles'; -import { UserAPIClient } from './user_api_client'; -import { UsersGridPage } from './users_grid'; -import { EditUserPage } from './edit_user'; interface CreateParams { authc: AuthenticationServiceSetup; @@ -30,7 +26,6 @@ export const usersManagementApp = Object.freeze({ order: 10, title: i18n.translate('xpack.security.management.usersTitle', { defaultMessage: 'Users' }), async mount({ basePath, element, setBreadcrumbs }) { - const [{ http, notifications, i18n: i18nStart }] = await getStartServices(); const usersBreadcrumbs = [ { text: i18n.translate('xpack.security.users.breadcrumb', { defaultMessage: 'Users' }), @@ -38,6 +33,20 @@ export const usersManagementApp = Object.freeze({ }, ]; + const [ + [{ http, notifications, i18n: i18nStart }], + { UsersGridPage }, + { EditUserPage }, + { UserAPIClient }, + { RolesAPIClient }, + ] = await Promise.all([ + getStartServices(), + import('./users_grid'), + import('./edit_user'), + import('./user_api_client'), + import('../roles'), + ]); + const userAPIClient = new UserAPIClient(http); const rolesAPIClient = new RolesAPIClient(http); const UsersGridPageWithBreadcrumbs = () => { diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index ababf12c2be60..a6407366bbd3b 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -8,6 +8,7 @@ import { SecurityPluginSetup } from './plugin'; import { authenticationMock } from './authentication/index.mock'; import { authorizationMock } from './authorization/index.mock'; +import { licenseMock } from '../common/licensing/index.mock'; function createSetupMock() { const mockAuthz = authorizationMock.create(); @@ -19,6 +20,7 @@ function createSetupMock() { mode: mockAuthz.mode, }, registerSpacesService: jest.fn(), + license: licenseMock.create(), __legacyCompat: {} as SecurityPluginSetup['__legacyCompat'], }; } diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 22a30f03c646a..d58c999ddccdf 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -50,21 +50,6 @@ describe('Security Plugin', () => { await expect(plugin.setup(mockCoreSetup, mockDependencies)).resolves.toMatchInlineSnapshot(` Object { "__legacyCompat": Object { - "license": Object { - "features$": Observable { - "_isScalar": false, - "operator": MapOperator { - "project": [Function], - "thisArg": undefined, - }, - "source": Observable { - "_isScalar": false, - "_subscribe": [Function], - }, - }, - "getFeatures": [Function], - "isEnabled": [Function], - }, "registerLegacyAPI": [Function], "registerPrivilegesWithCluster": [Function], }, @@ -103,6 +88,21 @@ describe('Security Plugin', () => { "useRbacForRequest": [Function], }, }, + "license": Object { + "features$": Observable { + "_isScalar": false, + "operator": MapOperator { + "project": [Function], + "thisArg": undefined, + }, + "source": Observable { + "_isScalar": false, + "_subscribe": [Function], + }, + }, + "getFeatures": [Function], + "isEnabled": [Function], + }, "registerSpacesService": [Function], } `); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index e30b0caf76ddc..97f5aea888dc7 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -59,6 +59,7 @@ export interface SecurityPluginSetup { | 'invalidateAPIKeyAsInternalUser' >; authz: Pick; + license: SecurityLicense; /** * If Spaces plugin is available it's supposed to register its SpacesService with Security plugin @@ -73,7 +74,6 @@ export interface SecurityPluginSetup { __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => void; registerPrivilegesWithCluster: () => void; - license: SecurityLicense; }; } @@ -194,6 +194,8 @@ export class Plugin { mode: authz.mode, }, + license, + registerSpacesService: service => { if (this.wasSpacesServiceAccessed()) { throw new Error('Spaces service has been accessed before registration.'); @@ -206,8 +208,6 @@ export class Plugin { registerLegacyAPI: (legacyAPI: LegacyAPI) => (this.legacyAPI = legacyAPI), registerPrivilegesWithCluster: async () => await authz.registerPrivilegesWithCluster(), - - license, }, }); } diff --git a/x-pack/plugins/siem/server/lib/timeline/types.ts b/x-pack/plugins/siem/common/types/timeline/index.ts similarity index 85% rename from x-pack/plugins/siem/server/lib/timeline/types.ts rename to x-pack/plugins/siem/common/types/timeline/index.ts index 0bce3300591c2..55b4f9c6aca4d 100644 --- a/x-pack/plugins/siem/server/lib/timeline/types.ts +++ b/x-pack/plugins/siem/common/types/timeline/index.ts @@ -7,14 +7,11 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ import * as runtimeTypes from 'io-ts'; +import { SavedObjectsClient } from 'kibana/server'; -import { unionWithNullType } from '../framework'; -import { NoteSavedObjectToReturnRuntimeType, NoteSavedObject } from '../note/types'; -import { - PinnedEventToReturnSavedObjectRuntimeType, - PinnedEventSavedObject, -} from '../pinned_event/types'; -import { SavedObjectsClient } from '../../../../../../src/core/server'; +import { unionWithNullType } from '../../utility_types'; +import { NoteSavedObject, NoteSavedObjectToReturnRuntimeType } from './note'; +import { PinnedEventToReturnSavedObjectRuntimeType, PinnedEventSavedObject } from './pinned_event'; /* * ColumnHeader Types @@ -136,6 +133,17 @@ const SavedSortRuntimeType = runtimeTypes.partial({ /* * Timeline Types */ + +export enum TimelineType { + default = 'default', + template = 'template', +} + +export const TimelineTypeLiteralRt = runtimeTypes.union([ + runtimeTypes.literal(TimelineType.template), + runtimeTypes.literal(TimelineType.default), +]); + export const SavedTimelineRuntimeType = runtimeTypes.partial({ columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)), dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), @@ -146,6 +154,9 @@ export const SavedTimelineRuntimeType = runtimeTypes.partial({ kqlMode: unionWithNullType(runtimeTypes.string), kqlQuery: unionWithNullType(SavedFilterQueryQueryRuntimeType), title: unionWithNullType(runtimeTypes.string), + templateTimelineId: unionWithNullType(runtimeTypes.string), + templateTimelineVersion: unionWithNullType(runtimeTypes.number), + timelineType: unionWithNullType(TimelineTypeLiteralRt), dateRange: unionWithNullType(SavedDateRangePickerRuntimeType), savedQueryId: unionWithNullType(runtimeTypes.string), sort: unionWithNullType(SavedSortRuntimeType), @@ -192,6 +203,25 @@ export const TimelineSavedToReturnObjectRuntimeType = runtimeTypes.intersection( export interface TimelineSavedObject extends runtimeTypes.TypeOf {} +/** + * All Timeline Saved object type with metadata + */ +export const TimelineResponseType = runtimeTypes.type({ + data: runtimeTypes.type({ + persistTimeline: runtimeTypes.intersection([ + runtimeTypes.partial({ + code: unionWithNullType(runtimeTypes.number), + message: unionWithNullType(runtimeTypes.string), + }), + runtimeTypes.type({ + timeline: TimelineSavedToReturnObjectRuntimeType, + }), + ]), + }), +}); + +export interface TimelineResponse extends runtimeTypes.TypeOf {} + /** * All Timeline Saved object type with metadata */ @@ -234,6 +264,11 @@ export type ExportedTimelines = TimelineSavedObject & pinnedEventIds: string[]; }; +export interface ExportTimelineNotFoundError { + statusCode: number; + message: string; +} + export interface BulkGetInput { type: string; id: string; diff --git a/x-pack/plugins/siem/server/lib/note/types.ts b/x-pack/plugins/siem/common/types/timeline/note/index.ts similarity index 90% rename from x-pack/plugins/siem/server/lib/note/types.ts rename to x-pack/plugins/siem/common/types/timeline/note/index.ts index f7a10317bd84d..c8e674997c19c 100644 --- a/x-pack/plugins/siem/server/lib/note/types.ts +++ b/x-pack/plugins/siem/common/types/timeline/note/index.ts @@ -8,7 +8,7 @@ import * as runtimeTypes from 'io-ts'; -import { unionWithNullType } from '../framework'; +import { unionWithNullType } from '../../../utility_types'; /* * Note Types @@ -56,11 +56,7 @@ export const NoteSavedObjectToReturnRuntimeType = runtimeTypes.intersection([ version: runtimeTypes.string, }), runtimeTypes.partial({ - timelineVersion: runtimeTypes.union([ - runtimeTypes.string, - runtimeTypes.null, - runtimeTypes.undefined, - ]), + timelineVersion: unionWithNullType(runtimeTypes.string), }), ]); diff --git a/x-pack/plugins/siem/server/lib/pinned_event/types.ts b/x-pack/plugins/siem/common/types/timeline/pinned_event/index.ts similarity index 83% rename from x-pack/plugins/siem/server/lib/pinned_event/types.ts rename to x-pack/plugins/siem/common/types/timeline/pinned_event/index.ts index e598f03935047..89a619598f7c1 100644 --- a/x-pack/plugins/siem/server/lib/pinned_event/types.ts +++ b/x-pack/plugins/siem/common/types/timeline/pinned_event/index.ts @@ -8,7 +8,7 @@ import * as runtimeTypes from 'io-ts'; -import { unionWithNullType } from '../framework'; +import { unionWithNullType } from '../../../utility_types'; /* * Note Types @@ -40,11 +40,7 @@ export const PinnedEventSavedObjectRuntimeType = runtimeTypes.intersection([ }), runtimeTypes.partial({ pinnedEventId: unionWithNullType(runtimeTypes.string), - timelineVersion: runtimeTypes.union([ - runtimeTypes.string, - runtimeTypes.null, - runtimeTypes.undefined, - ]), + timelineVersion: unionWithNullType(runtimeTypes.string), }), ]); @@ -55,11 +51,7 @@ export const PinnedEventToReturnSavedObjectRuntimeType = runtimeTypes.intersecti }), SavedPinnedEventRuntimeType, runtimeTypes.partial({ - timelineVersion: runtimeTypes.union([ - runtimeTypes.string, - runtimeTypes.null, - runtimeTypes.undefined, - ]), + timelineVersion: unionWithNullType(runtimeTypes.string), }), ]); diff --git a/x-pack/plugins/siem/common/utility_types.ts b/x-pack/plugins/siem/common/utility_types.ts index b46ccdbbe3d05..a12dd926a9181 100644 --- a/x-pack/plugins/siem/common/utility_types.ts +++ b/x-pack/plugins/siem/common/utility_types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import * as runtimeTypes from 'io-ts'; import { ReactNode } from 'react'; // This type is for typing EuiDescriptionList @@ -11,3 +12,6 @@ export interface DescriptionList { title: NonNullable; description: NonNullable; } + +export const unionWithNullType = (type: T) => + runtimeTypes.union([type, runtimeTypes.null]); diff --git a/x-pack/plugins/siem/public/components/and_or_badge/index.tsx b/x-pack/plugins/siem/public/components/and_or_badge/index.tsx index e2078bb2473f5..28355372df146 100644 --- a/x-pack/plugins/siem/public/components/and_or_badge/index.tsx +++ b/x-pack/plugins/siem/public/components/and_or_badge/index.tsx @@ -10,7 +10,7 @@ import styled from 'styled-components'; import * as i18n from './translations'; -const RoundedBadge = styled(EuiBadge)` +const RoundedBadge = (styled(EuiBadge)` align-items: center; border-radius: 100%; display: inline-flex; @@ -30,7 +30,7 @@ const RoundedBadge = styled(EuiBadge)` .euiBadge__text { text-overflow: clip; } -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any +` as unknown) as typeof EuiBadge; RoundedBadge.displayName = 'RoundedBadge'; diff --git a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx index ef2cd85667408..7c2d5e51d813f 100644 --- a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx +++ b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx @@ -17,10 +17,10 @@ import { import { FeatureProperty } from '../types'; import * as i18n from '../translations'; -const FlowBadge = styled(EuiBadge)` +const FlowBadge = (styled(EuiBadge)` height: 45px; min-width: 85px; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any +` as unknown) as typeof EuiBadge; const EuiFlexGroupStyled = styled(EuiFlexGroup)` margin: 0 auto; diff --git a/x-pack/plugins/siem/public/components/flyout/index.tsx b/x-pack/plugins/siem/public/components/flyout/index.tsx index b0f6494e2d663..404ca4a16e0f1 100644 --- a/x-pack/plugins/siem/public/components/flyout/index.tsx +++ b/x-pack/plugins/siem/public/components/flyout/index.tsx @@ -18,14 +18,14 @@ import { DEFAULT_TIMELINE_WIDTH } from '../timeline/body/constants'; import { StatefulTimeline } from '../timeline'; import { TimelineById } from '../../store/timeline/types'; -export const Badge = styled(EuiBadge)` +export const Badge = (styled(EuiBadge)` position: absolute; padding-left: 4px; padding-right: 4px; right: 0%; top: 0%; border-bottom-left-radius: 5px; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any +` as unknown) as typeof EuiBadge; Badge.displayName = 'Badge'; diff --git a/x-pack/plugins/siem/public/components/header_page/index.tsx b/x-pack/plugins/siem/public/components/header_page/index.tsx index acbb337a7c2ab..e1559cf9e0c48 100644 --- a/x-pack/plugins/siem/public/components/header_page/index.tsx +++ b/x-pack/plugins/siem/public/components/header_page/index.tsx @@ -52,9 +52,9 @@ const LinkBack = styled.div.attrs({ `; LinkBack.displayName = 'LinkBack'; -const Badge = styled(EuiBadge)` +const Badge = (styled(EuiBadge)` letter-spacing: 0; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any +` as unknown) as typeof EuiBadge; Badge.displayName = 'Badge'; interface BackOptions { diff --git a/x-pack/plugins/siem/public/components/header_page/title.tsx b/x-pack/plugins/siem/public/components/header_page/title.tsx index 47dd0dc55d703..43b50c24f6b5b 100644 --- a/x-pack/plugins/siem/public/components/header_page/title.tsx +++ b/x-pack/plugins/siem/public/components/header_page/title.tsx @@ -17,9 +17,9 @@ const StyledEuiBetaBadge = styled(EuiBetaBadge)` StyledEuiBetaBadge.displayName = 'StyledEuiBetaBadge'; -const Badge = styled(EuiBadge)` +const Badge = (styled(EuiBadge)` letter-spacing: 0; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any +` as unknown) as typeof EuiBadge; Badge.displayName = 'Badge'; interface Props { diff --git a/x-pack/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap index 5aa846d15b684..c4bdff7ea649a 100644 --- a/x-pack/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"

"`; +exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"
"`; -exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"
"`; +exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"
"`; diff --git a/x-pack/plugins/siem/public/components/open_timeline/export_timeline/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/export_timeline/index.tsx index 946c4b3a612dd..12cf952bb1ff8 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/export_timeline/index.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/export_timeline/index.tsx @@ -9,7 +9,7 @@ import { DeleteTimelines } from '../types'; import { TimelineDownloader } from './export_timeline'; import { DeleteTimelineModalOverlay } from '../delete_timeline_modal'; -import { exportSelectedTimeline } from '../../../containers/timeline/all/api'; +import { exportSelectedTimeline } from '../../../containers/timeline/api'; export interface ExportTimeline { disableExportTimelineDownloader: () => void; diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx index 04f0abe0d00d1..ea28bc06ef915 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx @@ -15,15 +15,34 @@ import { TestProviderWithoutDragAndDrop, apolloClient } from '../../mock/test_pr import { mockOpenTimelineQueryResults } from '../../mock/timeline_results'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines/timelines_page'; -import { StatefulOpenTimeline } from '.'; import { NotePreviews } from './note_previews'; import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; - +import { StatefulOpenTimeline } from '.'; +import { useGetAllTimeline, getAllTimeline } from '../../containers/timeline/all'; jest.mock('../../lib/kibana'); +jest.mock('../../containers/timeline/all', () => { + const originalModule = jest.requireActual('../../containers/timeline/all'); + return { + useGetAllTimeline: jest.fn(), + getAllTimeline: originalModule.getAllTimeline, + }; +}); describe('StatefulOpenTimeline', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); const title = 'All Timelines / Open Timelines'; + beforeEach(() => { + ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ + fetchAllTimeline: jest.fn(), + timelines: getAllTimeline( + '', + mockOpenTimelineQueryResults[0].result.data?.getAllTimeline?.timeline ?? [] + ), + loading: false, + totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, + refetch: jest.fn(), + }); + }); test('it has the expected initial state', () => { const wrapper = mount( @@ -459,6 +478,8 @@ describe('StatefulOpenTimeline', () => { .find('[data-test-subj="expand-notes"]') .first() .simulate('click'); + expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="updated-by"]').exists()).toEqual(true); expect( wrapper @@ -532,7 +553,7 @@ describe('StatefulOpenTimeline', () => { test('it renders the expected count of matching timelines when no query has been entered', async () => { const wrapper = mount( - + ( /** The requested field to sort on */ const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); + const { fetchAllTimeline, timelines, loading, totalCount, refetch } = useGetAllTimeline(); + /** Invoked when the user presses enters to submit the text in the search input */ const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => { setSearch(query.queryText.trim()); @@ -133,45 +134,41 @@ export const StatefulOpenTimelineComponent = React.memo( // } // }; - const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( - (timelineIds: string[]) => { - deleteTimelines(timelineIds, { - search, - pageInfo: { - pageIndex: pageIndex + 1, - pageSize, - }, - sort: { - sortField: sortField as SortFieldTimeline, - sortOrder: sortDirection as Direction, - }, - onlyUserFavorite: onlyFavorites, + const deleteTimelines: DeleteTimelines = useCallback( + async (timelineIds: string[]) => { + if (timelineIds.includes(timeline.savedObjectId || '')) { + createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); + } + await apolloClient.mutate< + DeleteTimelineMutation.Mutation, + DeleteTimelineMutation.Variables + >({ + mutation: deleteTimelineMutation, + fetchPolicy: 'no-cache', + variables: { id: timelineIds }, }); + refetch(); + }, + [apolloClient, createNewTimeline, refetch, timeline] + ); + + const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( + async (timelineIds: string[]) => { + await deleteTimelines(timelineIds); }, - [search, pageIndex, pageSize, sortField, sortDirection, onlyFavorites] + [deleteTimelines] ); /** Invoked when the user clicks the action to delete the selected timelines */ - const onDeleteSelected: OnDeleteSelected = useCallback(() => { - deleteTimelines(getSelectedTimelineIds(selectedItems), { - search, - pageInfo: { - pageIndex: pageIndex + 1, - pageSize, - }, - sort: { - sortField: sortField as SortFieldTimeline, - sortOrder: sortDirection as Direction, - }, - onlyUserFavorite: onlyFavorites, - }); + const onDeleteSelected: OnDeleteSelected = useCallback(async () => { + await deleteTimelines(getSelectedTimelineIds(selectedItems)); // NOTE: we clear the selection state below, but if the server fails to // delete a timeline, it will remain selected in the table: resetSelectionState(); // TODO: the query must re-execute to show the results of the deletion - }, [selectedItems, search, pageIndex, pageSize, sortField, sortDirection, onlyFavorites]); + }, [selectedItems, deleteTimelines]); /** Invoked when the user selects (or de-selects) timelines */ const onSelectionChange: OnSelectionChange = useCallback( @@ -227,99 +224,88 @@ export const StatefulOpenTimelineComponent = React.memo( [apolloClient, updateIsLoading, updateTimeline] ); - const deleteTimelines: DeleteTimelines = useCallback( - (timelineIds: string[], variables?: AllTimelinesVariables) => { - if (timelineIds.includes(timeline.savedObjectId || '')) { - createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); - } - apolloClient.mutate({ - mutation: deleteTimelineMutation, - fetchPolicy: 'no-cache', - variables: { id: timelineIds }, - refetchQueries: [ - { - query: allTimelinesQuery, - variables, - }, - ], - }); - }, - [apolloClient, createNewTimeline, timeline] - ); - useEffect(() => { focusInput(); }, []); - return ( - { + fetchAllTimeline({ + pageInfo: { pageIndex: pageIndex + 1, pageSize, - }} - search={search} - sort={{ sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction }} - onlyUserFavorite={onlyFavorites} - > - {({ timelines, loading, totalCount, refetch }) => { - return !isModal ? ( - - ) : ( - - ); - }} - + }, + search, + sort: { sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction }, + onlyUserFavorite: onlyFavorites, + timelines, + totalCount, + }); + }, [ + pageIndex, + pageSize, + search, + sortField, + sortDirection, + timelines, + totalCount, + onlyFavorites, + ]); + + return !isModal ? ( + + ) : ( + ); } ); @@ -328,7 +314,6 @@ const makeMapStateToProps = () => { const getTimeline = timelineSelectors.getTimelineByIdSelector(); const mapStateToProps = (state: State) => { const timeline = getTimeline(state, 'timeline-1') ?? timelineDefaults; - return { timeline, }; diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx index 6b2f953b82de4..26aeab87e3510 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx @@ -14,7 +14,7 @@ import { TimelinesTable } from './timelines_table'; import { TitleRow } from './title_row'; import { ImportDataModal } from '../import_data_modal'; import * as i18n from './translations'; -import { importTimelines } from '../../containers/timeline/all/api'; +import { importTimelines } from '../../containers/timeline/api'; import { UtilityBarGroup, diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx index ca8fa50c572fe..46a0d46c1e0d1 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx @@ -13,6 +13,7 @@ import { ThemeProvider } from 'styled-components'; import { wait } from '../../../lib/helpers'; import { TestProviderWithoutDragAndDrop } from '../../../mock/test_providers'; import { mockOpenTimelineQueryResults } from '../../../mock/timeline_results'; +import { useGetAllTimeline, getAllTimeline } from '../../../containers/timeline/all'; import { OpenTimelineModal } from '.'; @@ -20,9 +21,28 @@ jest.mock('../../../lib/kibana'); jest.mock('../../../utils/apollo_context', () => ({ useApolloClient: () => ({}), })); +jest.mock('../../../containers/timeline/all', () => { + const originalModule = jest.requireActual('../../../containers/timeline/all'); + return { + useGetAllTimeline: jest.fn(), + getAllTimeline: originalModule.getAllTimeline, + }; +}); describe('OpenTimelineModal', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); + beforeEach(() => { + ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ + fetchAllTimeline: jest.fn(), + timelines: getAllTimeline( + '', + mockOpenTimelineQueryResults[0].result.data?.getAllTimeline?.timeline ?? [] + ), + loading: false, + totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, + refetch: jest.fn(), + }); + }); test('it renders the expected modal', async () => { const wrapper = mount( diff --git a/x-pack/plugins/siem/public/components/open_timeline/types.ts b/x-pack/plugins/siem/public/components/open_timeline/types.ts index b7cc92ebd183f..41999c6249277 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/types.ts +++ b/x-pack/plugins/siem/public/components/open_timeline/types.ts @@ -9,6 +9,7 @@ import { AllTimelinesVariables } from '../../containers/timeline/all'; import { TimelineModel } from '../../store/timeline/model'; import { NoteResult } from '../../graphql/types'; import { Refetch } from '../../store/inputs/model'; +import { TimelineType } from '../../../common/types/timeline'; /** The users who added a timeline to favorites */ export interface FavoriteTimelineResult { @@ -47,6 +48,8 @@ export interface OpenTimelineResult { pinnedEventIds?: Readonly> | null; savedObjectId?: string | null; title?: string | null; + templateTimelineId?: string | null; + type?: TimelineType.template | TimelineType.default; updated?: number | null; updatedBy?: string | null; } diff --git a/x-pack/plugins/siem/public/components/page/index.tsx b/x-pack/plugins/siem/public/components/page/index.tsx index 44dc75cd6bad3..5feb2ef73c57f 100644 --- a/x-pack/plugins/siem/public/components/page/index.tsx +++ b/x-pack/plugins/siem/public/components/page/index.tsx @@ -165,9 +165,9 @@ export const Pane1FlexContent = styled.div` Pane1FlexContent.displayName = 'Pane1FlexContent'; -export const CountBadge = styled(EuiBadge)` +export const CountBadge = (styled(EuiBadge)` margin-left: 5px; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any +` as unknown) as typeof EuiBadge; CountBadge.displayName = 'CountBadge'; @@ -177,9 +177,9 @@ export const Spacer = styled.span` Spacer.displayName = 'Spacer'; -export const Badge = styled(EuiBadge)` +export const Badge = (styled(EuiBadge)` vertical-align: top; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any +` as unknown) as typeof EuiBadge; Badge.displayName = 'Badge'; diff --git a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx index 5b851701b973c..b641038f35ba6 100644 --- a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx +++ b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx @@ -6,11 +6,11 @@ import ApolloClient from 'apollo-client'; import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; -import { AllTimelinesQuery } from '../../containers/timeline/all'; +import { useGetAllTimeline } from '../../containers/timeline/all'; import { SortFieldTimeline, Direction } from '../../graphql/types'; import { queryTimelineById, dispatchUpdateTimeline } from '../open_timeline/helpers'; import { OnOpenTimeline } from '../open_timeline/types'; @@ -62,35 +62,39 @@ const StatefulRecentTimelinesComponent = React.memo( [filterBy] ); - return ( - { + fetchAllTimeline({ + pageInfo: { pageIndex: 1, pageSize: PAGE_SIZE, - }} - search={''} - sort={{ + }, + search: '', + sort: { sortField: SortFieldTimeline.updated, sortOrder: Direction.desc, - }} - onlyUserFavorite={filterBy === 'favorites'} - > - {({ timelines, loading }) => ( - <> - {loading ? ( - loadingPlaceholders - ) : ( - - )} - - {linkAllTimelines} - + }, + onlyUserFavorite: filterBy === 'favorites', + timelines, + totalCount, + }); + }, [filterBy, timelines, totalCount]); + + return ( + <> + {loading ? ( + loadingPlaceholders + ) : ( + )} - + + {linkAllTimelines} + ); } ); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx index d05ab6bcc378f..66c559729cccd 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -28,9 +28,9 @@ const SignatureFlexItem = styled(EuiFlexItem)` SignatureFlexItem.displayName = 'SignatureFlexItem'; -const Badge = styled(EuiBadge)` +const Badge = (styled(EuiBadge)` vertical-align: top; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any +` as unknown) as typeof EuiBadge; Badge.displayName = 'Badge'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx index 31525a4904bc2..4cb8140e22cef 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -19,9 +19,9 @@ import { IS_OPERATOR } from '../../../data_providers/data_provider'; import * as i18n from './translations'; -const Badge = styled(EuiBadge)` +const Badge = (styled(EuiBadge)` vertical-align: top; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any +` as unknown) as typeof EuiBadge; Badge.displayName = 'Badge'; diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/empty.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/empty.tsx index ddb07b4636b88..60c868f780ff3 100644 --- a/x-pack/plugins/siem/public/components/timeline/data_providers/empty.tsx +++ b/x-pack/plugins/siem/public/components/timeline/data_providers/empty.tsx @@ -21,12 +21,12 @@ const Text = styled(EuiText)` Text.displayName = 'Text'; -const BadgeHighlighted = styled(EuiBadge)` +const BadgeHighlighted = (styled(EuiBadge)` height: 20px; margin: 0 5px 0 5px; maxwidth: 85px; minwidth: 85px; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any +` as unknown) as typeof EuiBadge; BadgeHighlighted.displayName = 'BadgeHighlighted'; diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx index a5525df5ef3c3..e04aed17c6d67 100644 --- a/x-pack/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx +++ b/x-pack/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx @@ -18,7 +18,7 @@ import { EXISTS_OPERATOR, QueryOperator } from './data_provider'; import * as i18n from './translations'; -const ProviderBadgeStyled = styled(EuiBadge)` +const ProviderBadgeStyled = (styled(EuiBadge)` .euiToolTipAnchor { &::after { font-style: normal; @@ -44,7 +44,7 @@ const ProviderBadgeStyled = styled(EuiBadge)` margin-right: 0; margin-left: 4px; } -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any +` as unknown) as typeof EuiBadge; ProviderBadgeStyled.displayName = 'ProviderBadgeStyled'; diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx index 0ae952412a973..3a691d2bbc621 100644 --- a/x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx +++ b/x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx @@ -54,9 +54,9 @@ const DropAndTargetDataProviders = styled.div<{ hasAndItem: boolean }>` DropAndTargetDataProviders.displayName = 'DropAndTargetDataProviders'; -const NumberProviderAndBadge = styled(EuiBadge)` +const NumberProviderAndBadge = (styled(EuiBadge)` margin: 0px 5px; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any +` as unknown) as typeof EuiBadge; NumberProviderAndBadge.displayName = 'NumberProviderAndBadge'; diff --git a/x-pack/plugins/siem/public/components/timeline/properties/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/properties/helpers.tsx index bf953cfd006aa..4c64c8a100b41 100644 --- a/x-pack/plugins/siem/public/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/siem/public/components/timeline/properties/helpers.tsx @@ -37,9 +37,9 @@ export const historyToolTip = 'The chronological history of actions related to t export const streamLiveToolTip = 'Update the Timeline as new data arrives'; export const newTimelineToolTip = 'Create a new timeline'; -const NotesCountBadge = styled(EuiBadge)` +const NotesCountBadge = (styled(EuiBadge)` margin-left: 5px; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any +` as unknown) as typeof EuiBadge; NotesCountBadge.displayName = 'NotesCountBadge'; diff --git a/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx index 639d30bbe7bb9..4cc89e5bdba73 100644 --- a/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx +++ b/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx @@ -17,11 +17,11 @@ import { EuiFilterButton, } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; -import React, { memo, useCallback, useMemo, useState } from 'react'; +import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; import { ListProps } from 'react-virtualized'; import styled from 'styled-components'; -import { AllTimelinesQuery } from '../../../containers/timeline/all'; +import { useGetAllTimeline } from '../../../containers/timeline/all'; import { SortFieldTimeline, Direction } from '../../../graphql/types'; import { isUntitled } from '../../open_timeline/helpers'; import * as i18nTimeline from '../../open_timeline/translations'; @@ -96,6 +96,7 @@ const SelectableTimelineComponent: React.FC = ({ const [searchTimelineValue, setSearchTimelineValue] = useState(''); const [onlyFavorites, setOnlyFavorites] = useState(false); const [searchRef, setSearchRef] = useState(null); + const { fetchAllTimeline, timelines, loading, totalCount: timelineCount } = useGetAllTimeline(); const onSearchTimeline = useCallback(val => { setSearchTimelineValue(val); @@ -215,61 +216,64 @@ const SelectableTimelineComponent: React.FC = ({ [searchRef, onlyFavorites, handleOnToggleOnlyFavorites] ); + useEffect(() => { + fetchAllTimeline({ + pageInfo: { + pageIndex: 1, + pageSize, + }, + search: searchTimelineValue, + sort: { + sortField: SortFieldTimeline.updated, + sortOrder: Direction.desc, + }, + onlyUserFavorite: onlyFavorites, + timelines, + totalCount: timelineCount, + }); + }, [onlyFavorites, pageSize, searchTimelineValue, timelines, timelineCount]); + return ( - <> - + !hideUntitled || t.title !== '').length, + timelineCount + ), + } as unknown) as ListProps, + }} + renderOption={renderTimelineOption} + onChange={handleTimelineChange} + searchable + searchProps={{ + 'data-test-subj': 'timeline-super-select-search-box', + isLoading: loading, + placeholder: i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER, + onSearch: onSearchTimeline, + incremental: false, + inputRef: (ref: HTMLElement) => { + setSearchRef(ref); + }, }} - search={searchTimelineValue} - sort={{ sortField: SortFieldTimeline.updated, sortOrder: Direction.desc }} - onlyUserFavorite={onlyFavorites} + singleSelection={true} + options={getSelectableOptions({ timelines, onlyFavorites, searchTimelineValue })} > - {({ timelines, loading, totalCount }) => ( - - !hideUntitled || t.title !== '').length, - totalCount - ), - } as unknown) as ListProps, - }} - renderOption={renderTimelineOption} - onChange={handleTimelineChange} - searchable - searchProps={{ - 'data-test-subj': 'timeline-super-select-search-box', - isLoading: loading, - placeholder: i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER, - onSearch: onSearchTimeline, - incremental: false, - inputRef: (ref: HTMLElement) => { - setSearchRef(ref); - }, - }} - singleSelection={true} - options={getSelectableOptions({ timelines, onlyFavorites, searchTimelineValue })} - > - {(list, search) => ( - <> - {search} - {favoritePortal} - {list} - - )} - - + {(list, search) => ( + <> + {search} + {favoritePortal} + {list} + )} - - + + ); }; diff --git a/x-pack/plugins/siem/public/containers/timeline/all/api.ts b/x-pack/plugins/siem/public/containers/timeline/all/api.ts deleted file mode 100644 index 09c8374bad113..0000000000000 --- a/x-pack/plugins/siem/public/containers/timeline/all/api.ts +++ /dev/null @@ -1,48 +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 { TIMELINE_IMPORT_URL, TIMELINE_EXPORT_URL } from '../../../../common/constants'; -import { ImportDataProps, ImportDataResponse } from '../../detection_engine/rules'; -import { KibanaServices } from '../../../lib/kibana'; -import { ExportSelectedData } from '../../../components/generic_downloader'; - -export const importTimelines = async ({ - fileToImport, - overwrite = false, - signal, -}: ImportDataProps): Promise => { - const formData = new FormData(); - formData.append('file', fileToImport); - - return KibanaServices.get().http.fetch(`${TIMELINE_IMPORT_URL}`, { - method: 'POST', - headers: { 'Content-Type': undefined }, - query: { overwrite }, - body: formData, - signal, - }); -}; - -export const exportSelectedTimeline: ExportSelectedData = async ({ - excludeExportDetails = false, - filename = `timelines_export.ndjson`, - ids = [], - signal, -}): Promise => { - const body = ids.length > 0 ? JSON.stringify({ ids }) : undefined; - const response = await KibanaServices.get().http.fetch(`${TIMELINE_EXPORT_URL}`, { - method: 'POST', - body, - query: { - exclude_export_details: excludeExportDetails, - file_name: filename, - }, - signal, - asResponse: true, - }); - - return response.body!; -}; diff --git a/x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts index e380e46e77070..7d30b6c22a110 100644 --- a/x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts +++ b/x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts @@ -55,6 +55,9 @@ export const allTimelinesQuery = gql` noteIds pinnedEventIds title + timelineType + templateTimelineId + templateTimelineVersion created createdBy updated diff --git a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx index b5c91ca287f0b..62c8d21a2e944 100644 --- a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx +++ b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx @@ -3,23 +3,28 @@ * 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 } from 'react'; -import { getOr } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; -import { Query } from 'react-apollo'; +import { getOr, noop } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import { useCallback, useState, useRef, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; -import { ApolloQueryResult } from 'apollo-client'; import { OpenTimelineResult } from '../../../components/open_timeline/types'; +import { errorToToaster, useStateToaster } from '../../../components/toasters'; import { GetAllTimeline, PageInfoTimeline, SortTimeline, TimelineResult, } from '../../../graphql/types'; +import { inputsModel, inputsActions } from '../../../store/inputs'; +import { useApolloClient } from '../../../utils/apollo_context'; + import { allTimelinesQuery } from './index.gql_query'; +import * as i18n from '../../../pages/timelines/translations'; export interface AllTimelinesArgs { + fetchAllTimeline: ({ onlyUserFavorite, pageInfo, search, sort }: AllTimelinesVariables) => void; timelines: OpenTimelineResult[]; loading: boolean; totalCount: number; @@ -31,17 +36,13 @@ export interface AllTimelinesVariables { pageInfo: PageInfoTimeline; search: string; sort: SortTimeline; + timelines: OpenTimelineResult[]; + totalCount: number; } -interface OwnProps extends AllTimelinesVariables { - children?: (args: AllTimelinesArgs) => React.ReactNode; -} - -type Refetch = ( - variables: GetAllTimeline.Variables | undefined -) => Promise>; +export const ALL_TIMELINE_QUERY_ID = 'FETCH_ALL_TIMELINES'; -const getAllTimeline = memoizeOne( +export const getAllTimeline = memoizeOne( (variables: string, timelines: TimelineResult[]): OpenTimelineResult[] => timelines.map(timeline => ({ created: timeline.created, @@ -76,41 +77,117 @@ const getAllTimeline = memoizeOne( })) ); -const AllTimelinesQueryComponent: React.FC = ({ - children, - onlyUserFavorite, - pageInfo, - search, - sort, -}) => { - const variables: GetAllTimeline.Variables = { - onlyUserFavorite, - pageInfo, - search, - sort, - }; - const handleRefetch = useCallback((refetch: Refetch) => refetch(variables), [variables]); +export const useGetAllTimeline = (): AllTimelinesArgs => { + const dispatch = useDispatch(); + const apolloClient = useApolloClient(); + const refetch = useRef(); + const [, dispatchToaster] = useStateToaster(); + const [allTimelines, setAllTimelines] = useState({ + fetchAllTimeline: noop, + loading: false, + refetch: refetch.current ?? noop, + totalCount: 0, + timelines: [], + }); + + const fetchAllTimeline = useCallback( + async ({ + onlyUserFavorite, + pageInfo, + search, + sort, + timelines, + totalCount, + }: AllTimelinesVariables) => { + let didCancel = false; + const abortCtrl = new AbortController(); - return ( - - query={allTimelinesQuery} - fetchPolicy="network-only" - notifyOnNetworkStatusChange - variables={variables} - > - {({ data, loading, refetch }) => - children!({ - loading, - refetch: handleRefetch.bind(null, refetch), - totalCount: getOr(0, 'getAllTimeline.totalCount', data), - timelines: getAllTimeline( - JSON.stringify(variables), - getOr([], 'getAllTimeline.timeline', data) - ), - }) - } - + const fetchData = async () => { + try { + if (apolloClient != null) { + setAllTimelines({ + ...allTimelines, + timelines: timelines ?? allTimelines.timelines, + totalCount: totalCount ?? allTimelines.totalCount, + loading: true, + }); + const variables: GetAllTimeline.Variables = { + onlyUserFavorite, + pageInfo, + search, + sort, + }; + const response = await apolloClient.query< + GetAllTimeline.Query, + GetAllTimeline.Variables + >({ + query: allTimelinesQuery, + fetchPolicy: 'network-only', + variables, + context: { + fetchOptions: { + abortSignal: abortCtrl.signal, + }, + }, + }); + if (!didCancel) { + dispatch( + inputsActions.setQuery({ + inputId: 'global', + id: ALL_TIMELINE_QUERY_ID, + loading: false, + refetch: refetch.current ?? noop, + inspect: null, + }) + ); + setAllTimelines({ + fetchAllTimeline, + loading: false, + refetch: refetch.current ?? noop, + totalCount: getOr(0, 'getAllTimeline.totalCount', response.data), + timelines: getAllTimeline( + JSON.stringify(variables), + getOr([], 'getAllTimeline.timeline', response.data) + ), + }); + } + } + } catch (error) { + if (!didCancel) { + errorToToaster({ + title: i18n.ERROR_FETCHING_TIMELINES_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + setAllTimelines({ + fetchAllTimeline, + loading: false, + refetch: noop, + totalCount: 0, + timelines: [], + }); + } + } + }; + refetch.current = fetchData; + fetchData(); + return () => { + didCancel = true; + abortCtrl.abort(); + }; + }, + [apolloClient, allTimelines] ); -}; -export const AllTimelinesQuery = React.memo(AllTimelinesQueryComponent); + useEffect(() => { + return () => { + dispatch(inputsActions.deleteOneQuery({ inputId: 'global', id: ALL_TIMELINE_QUERY_ID })); + }; + }, [dispatch]); + + return { + ...allTimelines, + fetchAllTimeline, + refetch: refetch.current ?? noop, + }; +}; diff --git a/x-pack/plugins/siem/public/containers/timeline/api.ts b/x-pack/plugins/siem/public/containers/timeline/api.ts new file mode 100644 index 0000000000000..023e2e6af9f88 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/timeline/api.ts @@ -0,0 +1,115 @@ +/* + * 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 { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { throwErrors } from '../../../../case/common/api'; +import { + SavedTimeline, + TimelineResponse, + TimelineResponseType, +} from '../../../common/types/timeline'; +import { TIMELINE_URL, TIMELINE_IMPORT_URL, TIMELINE_EXPORT_URL } from '../../../common/constants'; + +import { KibanaServices } from '../../lib/kibana'; +import { ExportSelectedData } from '../../components/generic_downloader'; + +import { createToasterPlainError } from '../case/utils'; +import { ImportDataProps, ImportDataResponse } from '../detection_engine/rules'; + +interface RequestPostTimeline { + timeline: SavedTimeline; + signal?: AbortSignal; +} + +interface RequestPatchTimeline extends RequestPostTimeline { + timelineId: T; + version: T; +} + +type RequestPersistTimeline = RequestPostTimeline & Partial>; + +const decodeTimelineResponse = (respTimeline?: TimelineResponse) => + pipe( + TimelineResponseType.decode(respTimeline), + fold(throwErrors(createToasterPlainError), identity) + ); + +const postTimeline = async ({ timeline }: RequestPostTimeline): Promise => { + const response = await KibanaServices.get().http.post(TIMELINE_URL, { + method: 'POST', + body: JSON.stringify({ timeline }), + }); + + return decodeTimelineResponse(response); +}; + +const patchTimeline = async ({ + timelineId, + timeline, + version, +}: RequestPatchTimeline): Promise => { + const response = await KibanaServices.get().http.patch(TIMELINE_URL, { + method: 'PATCH', + body: JSON.stringify({ timeline, timelineId, version }), + }); + + return decodeTimelineResponse(response); +}; + +export const persistTimeline = async ({ + timelineId, + timeline, + version, +}: RequestPersistTimeline): Promise => { + if (timelineId == null) { + return postTimeline({ timeline }); + } + return patchTimeline({ + timelineId, + timeline, + version: version ?? '', + }); +}; + +export const importTimelines = async ({ + fileToImport, + overwrite = false, + signal, +}: ImportDataProps): Promise => { + const formData = new FormData(); + formData.append('file', fileToImport); + + return KibanaServices.get().http.fetch(`${TIMELINE_IMPORT_URL}`, { + method: 'POST', + headers: { 'Content-Type': undefined }, + query: { overwrite }, + body: formData, + signal, + }); +}; + +export const exportSelectedTimeline: ExportSelectedData = async ({ + excludeExportDetails = false, + filename = `timelines_export.ndjson`, + ids = [], + signal, +}): Promise => { + const body = ids.length > 0 ? JSON.stringify({ ids }) : undefined; + const response = await KibanaServices.get().http.fetch(`${TIMELINE_EXPORT_URL}`, { + method: 'POST', + body, + query: { + exclude_export_details: excludeExportDetails, + file_name: filename, + }, + signal, + asResponse: true, + }); + + return response.body!; +}; diff --git a/x-pack/plugins/siem/public/graphql/introspection.json b/x-pack/plugins/siem/public/graphql/introspection.json index 2a9dd8f2aacfe..4026a043c7778 100644 --- a/x-pack/plugins/siem/public/graphql/introspection.json +++ b/x-pack/plugins/siem/public/graphql/introspection.json @@ -9728,6 +9728,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "templateTimelineId", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "templateTimelineVersion", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timelineType", + "description": "", + "args": [], + "type": { "kind": "ENUM", "name": "TimelineType", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "updated", "description": "", @@ -10323,6 +10347,39 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "TimelineType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "default", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "template", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "PageInfoTimeline", @@ -10863,6 +10920,24 @@ "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": null }, + { + "name": "templateTimelineId", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "templateTimelineVersion", + "description": "", + "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, + "defaultValue": null + }, + { + "name": "timelineType", + "description": "", + "type": { "kind": "ENUM", "name": "TimelineType", "ofType": null }, + "defaultValue": null + }, { "name": "dateRange", "description": "", diff --git a/x-pack/plugins/siem/public/graphql/types.ts b/x-pack/plugins/siem/public/graphql/types.ts index e15c099a007ad..8c39d5e58b99e 100644 --- a/x-pack/plugins/siem/public/graphql/types.ts +++ b/x-pack/plugins/siem/public/graphql/types.ts @@ -132,6 +132,12 @@ export interface TimelineInput { title?: Maybe; + templateTimelineId?: Maybe; + + templateTimelineVersion?: Maybe; + + timelineType?: Maybe; + dateRange?: Maybe; savedQueryId?: Maybe; @@ -334,6 +340,11 @@ export enum TlsFields { _id = '_id', } +export enum TimelineType { + default = 'default', + template = 'template', +} + export enum SortFieldTimeline { title = 'title', description = 'description', @@ -1944,6 +1955,12 @@ export interface TimelineResult { title?: Maybe; + templateTimelineId?: Maybe; + + templateTimelineVersion?: Maybe; + + timelineType?: Maybe; + updated?: Maybe; updatedBy?: Maybe; @@ -4030,6 +4047,12 @@ export namespace GetAllTimeline { title: Maybe; + timelineType: Maybe; + + templateTimelineId: Maybe; + + templateTimelineVersion: Maybe; + created: Maybe; createdBy: Maybe; diff --git a/x-pack/plugins/siem/public/lib/telemetry/middleware.ts b/x-pack/plugins/siem/public/lib/telemetry/middleware.ts index 59c6cb3566907..ca889e20e695f 100644 --- a/x-pack/plugins/siem/public/lib/telemetry/middleware.ts +++ b/x-pack/plugins/siem/public/lib/telemetry/middleware.ts @@ -7,7 +7,7 @@ import { Action, Dispatch, MiddlewareAPI } from 'redux'; import { track, METRIC_TYPE, TELEMETRY_EVENT } from './'; -import { timelineActions } from '../../store/actions'; +import * as timelineActions from '../../store/timeline/actions'; export const telemetryMiddleware = (api: MiddlewareAPI) => (next: Dispatch) => (action: Action) => { if (timelineActions.endTimelineSaving.match(action)) { diff --git a/x-pack/plugins/siem/public/mock/timeline_results.ts b/x-pack/plugins/siem/public/mock/timeline_results.ts index 0e61457873bfd..edd1c73771829 100644 --- a/x-pack/plugins/siem/public/mock/timeline_results.ts +++ b/x-pack/plugins/siem/public/mock/timeline_results.ts @@ -168,6 +168,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 1', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -294,6 +297,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 2', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -420,6 +426,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 2', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -546,6 +555,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 3', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -672,6 +684,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 4', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -798,6 +813,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 5', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -924,6 +942,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 6', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1050,6 +1071,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1176,6 +1200,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1302,6 +1329,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1428,6 +1458,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1554,6 +1587,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1680,6 +1716,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx index e54441f0118a1..1ac371a3f6829 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -37,11 +37,11 @@ const NoteDescriptionContainer = styled(EuiFlexItem)` export const isNotEmptyArray = (values: string[]) => !isEmpty(values.join('')); -const EuiBadgeWrap = styled(EuiBadge)` +const EuiBadgeWrap = (styled(EuiBadge)` .euiBadge__text { white-space: pre-wrap !important; } -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any +` as unknown) as typeof EuiBadge; export const buildQueryBarDescription = ({ field, diff --git a/x-pack/plugins/siem/public/pages/timelines/translations.ts b/x-pack/plugins/siem/public/pages/timelines/translations.ts index 723d164ad2cdd..304474bbff2c5 100644 --- a/x-pack/plugins/siem/public/pages/timelines/translations.ts +++ b/x-pack/plugins/siem/public/pages/timelines/translations.ts @@ -23,3 +23,10 @@ export const ALL_TIMELINES_IMPORT_TIMELINE_TITLE = i18n.translate( defaultMessage: 'Import Timeline', } ); + +export const ERROR_FETCHING_TIMELINES_TITLE = i18n.translate( + 'xpack.siem.timelines.allTimelines.errorFetchingTimelinesTitle', + { + defaultMessage: 'Failed to query all timelines data', + } +); diff --git a/x-pack/plugins/siem/public/store/model.ts b/x-pack/plugins/siem/public/store/model.ts index 9e9e663a59fe0..686dc096e61b0 100644 --- a/x-pack/plugins/siem/public/store/model.ts +++ b/x-pack/plugins/siem/public/store/model.ts @@ -9,15 +9,4 @@ export { dragAndDropModel } from './drag_and_drop'; export { hostsModel } from './hosts'; export { inputsModel } from './inputs'; export { networkModel } from './network'; - -export type KueryFilterQueryKind = 'kuery' | 'lucene'; - -export interface KueryFilterQuery { - kind: KueryFilterQueryKind; - expression: string; -} - -export interface SerializedFilterQuery { - kuery: KueryFilterQuery | null; - serializedQuery: string; -} +export * from './types'; diff --git a/x-pack/plugins/siem/public/store/store.ts b/x-pack/plugins/siem/public/store/store.ts index d3559e7a7adde..2af0f87b4494d 100644 --- a/x-pack/plugins/siem/public/store/store.ts +++ b/x-pack/plugins/siem/public/store/store.ts @@ -32,6 +32,7 @@ export const createStore = ( const middlewareDependencies = { apolloClient$: apolloClient, + selectAllTimelineQuery: inputsSelectors.globalQueryByIdSelector, selectNotesByIdSelector: appSelectors.selectNotesByIdSelector, timelineByIdSelector: timelineSelectors.timelineByIdSelector, timelineTimeRangeSelector: inputsSelectors.timelineTimeRangeSelector, diff --git a/x-pack/plugins/siem/public/store/timeline/actions.ts b/x-pack/plugins/siem/public/store/timeline/actions.ts index a03cc2643e014..12155decf40d4 100644 --- a/x-pack/plugins/siem/public/store/timeline/actions.ts +++ b/x-pack/plugins/siem/public/store/timeline/actions.ts @@ -12,7 +12,7 @@ import { DataProvider, QueryOperator, } from '../../components/timeline/data_providers/data_provider'; -import { KueryFilterQuery, SerializedFilterQuery } from '../model'; +import { KueryFilterQuery, SerializedFilterQuery } from '../types'; import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; import { TimelineNonEcsData } from '../../graphql/types'; diff --git a/x-pack/plugins/siem/public/store/timeline/epic.ts b/x-pack/plugins/siem/public/store/timeline/epic.ts index 4fece0572274f..6812d8d8aa672 100644 --- a/x-pack/plugins/siem/public/store/timeline/epic.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic.ts @@ -29,17 +29,11 @@ import { } from 'rxjs/operators'; import { esFilters, Filter, MatchAllFilter } from '../../../../../../src/plugins/data/public'; -import { persistTimelineMutation } from '../../containers/timeline/persist.gql_query'; -import { - PersistTimelineMutation, - TimelineInput, - ResponseTimeline, - TimelineResult, -} from '../../graphql/types'; +import { TimelineInput, ResponseTimeline, TimelineResult } from '../../graphql/types'; import { AppApolloClient } from '../../lib/lib'; import { addError } from '../app/actions'; import { NotesById } from '../app/model'; -import { TimeRange } from '../inputs/model'; +import { inputsModel } from '../inputs'; import { applyKqlFilterQuery, @@ -75,13 +69,15 @@ import { epicPersistPinnedEvent, timelinePinnedEventActionsType } from './epic_p import { epicPersistTimelineFavorite, timelineFavoriteActionsType } from './epic_favorite'; import { isNotNull } from './helpers'; import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue'; -import { refetchQueries } from './refetch_queries'; import { myEpicTimelineId } from './my_epic_timeline_id'; import { ActionTimeline, TimelineById } from './types'; +import { persistTimeline } from '../../containers/timeline/api'; +import { ALL_TIMELINE_QUERY_ID } from '../../containers/timeline/all'; interface TimelineEpicDependencies { timelineByIdSelector: (state: State) => TimelineById; - timelineTimeRangeSelector: (state: State) => TimeRange; + timelineTimeRangeSelector: (state: State) => inputsModel.TimeRange; + selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery; selectNotesByIdSelector: (state: State) => NotesById; apolloClient$: Observable; } @@ -119,10 +115,24 @@ export const createTimelineEpic = (): Epic< > => ( action$, state$, - { selectNotesByIdSelector, timelineByIdSelector, timelineTimeRangeSelector, apolloClient$ } + { + selectAllTimelineQuery, + selectNotesByIdSelector, + timelineByIdSelector, + timelineTimeRangeSelector, + apolloClient$, + } ) => { const timeline$ = state$.pipe(map(timelineByIdSelector), filter(isNotNull)); + const allTimelineQuery$ = state$.pipe( + map(state => { + const getQuery = selectAllTimelineQuery(); + return getQuery(state, ALL_TIMELINE_QUERY_ID); + }), + filter(isNotNull) + ); + const notes$ = state$.pipe(map(selectNotesByIdSelector), filter(isNotNull)); const timelineTimeRange$ = state$.pipe(map(timelineTimeRangeSelector), filter(isNotNull)); @@ -168,33 +178,52 @@ export const createTimelineEpic = (): Epic< const version = myEpicTimelineId.getTimelineVersion(); if (timelineNoteActionsType.includes(action.type)) { - return epicPersistNote(apolloClient, action, timeline, notes, action$, timeline$, notes$); + return epicPersistNote( + apolloClient, + action, + timeline, + notes, + action$, + timeline$, + notes$, + allTimelineQuery$ + ); } else if (timelinePinnedEventActionsType.includes(action.type)) { - return epicPersistPinnedEvent(apolloClient, action, timeline, action$, timeline$); + return epicPersistPinnedEvent( + apolloClient, + action, + timeline, + action$, + timeline$, + allTimelineQuery$ + ); } else if (timelineFavoriteActionsType.includes(action.type)) { - return epicPersistTimelineFavorite(apolloClient, action, timeline, action$, timeline$); + return epicPersistTimelineFavorite( + apolloClient, + action, + timeline, + action$, + timeline$, + allTimelineQuery$ + ); } else if (timelineActionsType.includes(action.type)) { return from( - apolloClient.mutate< - PersistTimelineMutation.Mutation, - PersistTimelineMutation.Variables - >({ - mutation: persistTimelineMutation, - fetchPolicy: 'no-cache', - variables: { - timelineId, - version, - timeline: convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange), - }, - refetchQueries, + persistTimeline({ + timelineId, + version, + timeline: convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange), }) ).pipe( - withLatestFrom(timeline$), - mergeMap(([result, recentTimeline]) => { + withLatestFrom(timeline$, allTimelineQuery$), + mergeMap(([result, recentTimeline, allTimelineQuery]) => { const savedTimeline = recentTimeline[action.payload.id]; const response: ResponseTimeline = get('data.persistTimeline', result); const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; + if (allTimelineQuery.refetch != null) { + (allTimelineQuery.refetch as inputsModel.Refetch)(); + } + return [ response.code === 409 ? updateAutoSaveMsg({ @@ -261,7 +290,7 @@ const timelineInput: TimelineInput = { export const convertTimelineAsInput = ( timeline: TimelineModel, - timelineTimeRange: TimeRange + timelineTimeRange: inputsModel.TimeRange ): TimelineInput => Object.keys(timelineInput).reduce((acc, key) => { if (has(key, timeline)) { diff --git a/x-pack/plugins/siem/public/store/timeline/epic_favorite.ts b/x-pack/plugins/siem/public/store/timeline/epic_favorite.ts index 4d1b73aa70a6e..6a1dadb8a59f5 100644 --- a/x-pack/plugins/siem/public/store/timeline/epic_favorite.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic_favorite.ts @@ -26,6 +26,7 @@ import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persi import { refetchQueries } from './refetch_queries'; import { myEpicTimelineId } from './my_epic_timeline_id'; import { ActionTimeline, TimelineById } from './types'; +import { inputsModel } from '../inputs'; export const timelineFavoriteActionsType = [updateIsFavorite.type]; @@ -34,7 +35,8 @@ export const epicPersistTimelineFavorite = ( action: ActionTimeline, timeline: TimelineById, action$: Observable, - timeline$: Observable + timeline$: Observable, + allTimelineQuery$: Observable // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Observable => from( @@ -50,12 +52,16 @@ export const epicPersistTimelineFavorite = ( refetchQueries, }) ).pipe( - withLatestFrom(timeline$), - mergeMap(([result, recentTimelines]) => { + withLatestFrom(timeline$, allTimelineQuery$), + mergeMap(([result, recentTimelines, allTimelineQuery]) => { const savedTimeline = recentTimelines[action.payload.id]; const response: ResponseFavoriteTimeline = get('data.persistFavorite', result); const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; + if (allTimelineQuery.refetch != null) { + (allTimelineQuery.refetch as inputsModel.Refetch)(); + } + return [ ...callOutMsg, updateTimeline({ diff --git a/x-pack/plugins/siem/public/store/timeline/epic_note.ts b/x-pack/plugins/siem/public/store/timeline/epic_note.ts index e5a712fe2c666..3722a6ad8036c 100644 --- a/x-pack/plugins/siem/public/store/timeline/epic_note.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic_note.ts @@ -16,6 +16,7 @@ import { persistTimelineNoteMutation } from '../../containers/timeline/notes/per import { PersistTimelineNoteMutation, ResponseNote } from '../../graphql/types'; import { updateNote, addError } from '../app/actions'; import { NotesById } from '../app/model'; +import { inputsModel } from '../inputs'; import { addNote, @@ -39,7 +40,8 @@ export const epicPersistNote = ( notes: NotesById, action$: Observable, timeline$: Observable, - notes$: Observable + notes$: Observable, + allTimelineQuery$: Observable // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Observable => from( @@ -61,12 +63,16 @@ export const epicPersistNote = ( refetchQueries, }) ).pipe( - withLatestFrom(timeline$, notes$), - mergeMap(([result, recentTimeline, recentNotes]) => { + withLatestFrom(timeline$, notes$, allTimelineQuery$), + mergeMap(([result, recentTimeline, recentNotes, allTimelineQuery]) => { const noteIdRedux = action.payload.noteId; const response: ResponseNote = get('data.persistNote', result); const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; + if (allTimelineQuery.refetch != null) { + (allTimelineQuery.refetch as inputsModel.Refetch)(); + } + return [ ...callOutMsg, recentTimeline[action.payload.id].savedObjectId == null diff --git a/x-pack/plugins/siem/public/store/timeline/epic_pinned_event.ts b/x-pack/plugins/siem/public/store/timeline/epic_pinned_event.ts index 2260999a91e7b..a1281250ba72a 100644 --- a/x-pack/plugins/siem/public/store/timeline/epic_pinned_event.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic_pinned_event.ts @@ -15,6 +15,8 @@ import { filter, mergeMap, startWith, withLatestFrom, takeUntil } from 'rxjs/ope import { persistTimelinePinnedEventMutation } from '../../containers/timeline/pinned_event/persist.gql_query'; import { PersistTimelinePinnedEventMutation, PinnedEvent } from '../../graphql/types'; import { addError } from '../app/actions'; +import { inputsModel } from '../inputs'; + import { pinEvent, endTimelineSaving, @@ -35,7 +37,8 @@ export const epicPersistPinnedEvent = ( action: ActionTimeline, timeline: TimelineById, action$: Observable, - timeline$: Observable + timeline$: Observable, + allTimelineQuery$: Observable // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Observable => from( @@ -57,12 +60,16 @@ export const epicPersistPinnedEvent = ( refetchQueries, }) ).pipe( - withLatestFrom(timeline$), - mergeMap(([result, recentTimeline]) => { + withLatestFrom(timeline$, allTimelineQuery$), + mergeMap(([result, recentTimeline, allTimelineQuery]) => { const savedTimeline = recentTimeline[action.payload.id]; const response: PinnedEvent = get('data.persistPinnedEventOnTimeline', result); const callOutMsg = response && response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; + if (allTimelineQuery.refetch != null) { + (allTimelineQuery.refetch as inputsModel.Refetch)(); + } + return [ response != null ? updateTimeline({ diff --git a/x-pack/plugins/siem/public/store/types.ts b/x-pack/plugins/siem/public/store/types.ts new file mode 100644 index 0000000000000..2c679ba41116e --- /dev/null +++ b/x-pack/plugins/siem/public/store/types.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. + */ + +export type KueryFilterQueryKind = 'kuery' | 'lucene'; + +export interface KueryFilterQuery { + kind: KueryFilterQueryKind; + expression: string; +} + +export interface SerializedFilterQuery { + kuery: KueryFilterQuery | null; + serializedQuery: string; +} diff --git a/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts index 9dd04247b7f47..bc2b3a53d85f3 100644 --- a/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts @@ -125,6 +125,11 @@ export const timelineSchema = gql` script: String } + enum TimelineType { + default + template + } + input TimelineInput { columns: [ColumnHeaderInput!] dataProviders: [DataProviderInput!] @@ -134,6 +139,9 @@ export const timelineSchema = gql` kqlMode: String kqlQuery: SerializedFilterQueryInput title: String + templateTimelineId: String + templateTimelineVersion: Int + timelineType: TimelineType dateRange: DateRangePickerInput savedQueryId: String sort: SortTimelineInput @@ -237,6 +245,9 @@ export const timelineSchema = gql` savedObjectId: String! sort: SortTimelineResult title: String + templateTimelineId: String + templateTimelineVersion: Int + timelineType: TimelineType updated: Float updatedBy: String version: String! diff --git a/x-pack/plugins/siem/server/graphql/types.ts b/x-pack/plugins/siem/server/graphql/types.ts index d272b7ff59b79..6a35ba08f8e43 100644 --- a/x-pack/plugins/siem/server/graphql/types.ts +++ b/x-pack/plugins/siem/server/graphql/types.ts @@ -134,6 +134,12 @@ export interface TimelineInput { title?: Maybe; + templateTimelineId?: Maybe; + + templateTimelineVersion?: Maybe; + + timelineType?: Maybe; + dateRange?: Maybe; savedQueryId?: Maybe; @@ -336,6 +342,11 @@ export enum TlsFields { _id = '_id', } +export enum TimelineType { + default = 'default', + template = 'template', +} + export enum SortFieldTimeline { title = 'title', description = 'description', @@ -1946,6 +1957,12 @@ export interface TimelineResult { title?: Maybe; + templateTimelineId?: Maybe; + + templateTimelineVersion?: Maybe; + + timelineType?: Maybe; + updated?: Maybe; updatedBy?: Maybe; @@ -8023,6 +8040,12 @@ export namespace TimelineResultResolvers { title?: TitleResolver, TypeParent, TContext>; + templateTimelineId?: TemplateTimelineIdResolver, TypeParent, TContext>; + + templateTimelineVersion?: TemplateTimelineVersionResolver, TypeParent, TContext>; + + timelineType?: TimelineTypeResolver, TypeParent, TContext>; + updated?: UpdatedResolver, TypeParent, TContext>; updatedBy?: UpdatedByResolver, TypeParent, TContext>; @@ -8130,6 +8153,21 @@ export namespace TimelineResultResolvers { Parent = TimelineResult, TContext = SiemContext > = Resolver; + export type TemplateTimelineIdResolver< + R = Maybe, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver; + export type TemplateTimelineVersionResolver< + R = Maybe, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver; + export type TimelineTypeResolver< + R = Maybe, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver; export type UpdatedResolver< R = Maybe, Parent = TimelineResult, diff --git a/x-pack/plugins/siem/server/lib/compose/kibana.ts b/x-pack/plugins/siem/server/lib/compose/kibana.ts index 9c46f3320e37e..4a595032e43eb 100644 --- a/x-pack/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/plugins/siem/server/lib/compose/kibana.ts @@ -27,9 +27,9 @@ import { ElasticsearchSourceStatusAdapter, SourceStatus } from '../source_status import { ConfigurationSourcesAdapter, Sources } from '../sources'; import { AppBackendLibs, AppDomainLibs } from '../types'; import { ElasticsearchUncommonProcessesAdapter, UncommonProcesses } from '../uncommon_processes'; -import { Note } from '../note/saved_object'; -import { PinnedEvent } from '../pinned_event/saved_object'; -import { Timeline } from '../timeline/saved_object'; +import * as note from '../note/saved_object'; +import * as pinnedEvent from '../pinned_event/saved_object'; +import * as timeline from '../timeline/saved_object'; import { ElasticsearchMatrixHistogramAdapter, MatrixHistogram } from '../matrix_histogram'; export function compose( @@ -41,10 +41,6 @@ export function compose( const sources = new Sources(new ConfigurationSourcesAdapter()); const sourceStatus = new SourceStatus(new ElasticsearchSourceStatusAdapter(framework)); - const timeline = new Timeline(); - const note = new Note(); - const pinnedEvent = new PinnedEvent(); - const domainLibs: AppDomainLibs = { authentications: new Authentications(new ElasticsearchAuthenticationAdapter(framework)), events: new Events(new ElasticsearchEventsAdapter(framework)), diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json index cfc322788d4be..c83c0e01d7fa0 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json index 0647fe9c9ce10..18472abbd70d7 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], @@ -17,4 +17,4 @@ ], "type": "query", "version": 2 -} \ No newline at end of file +} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json index 036c88688d9bd..03024ad15396e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json index 0fe610d551152..e5a128029f585 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Credential Manipulation. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json index a317c77bcd90a..1c05743fae62f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Credential Manipulation. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json index 97640c0cea9b2..3396a8563ba1c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected an Exploit. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json index 069687a5af00f..2f70c539414c6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented an Exploit. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json index a7d3371190ced..cbf6c286a439f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Malware. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json index dd7bf72c34f90..49c7c160e5daf 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Malware. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json index a8e102cc4619d..e836bd037ddc5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Permission Theft. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json index c97330f2349eb..e9ac8d7ba6686 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Permission Theft. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json index e644c0e8d66eb..8e25832b0e89a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Process Injection. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json index 61cbe267f9a46..a59428275ca22 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Process Injection. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json index 0e88b26cb2c75..22091d8c9b68f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Ransomware. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json index ba341f059f26d..947bfcbba39a0 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Ransomware. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh index 750c5574f4a72..2028216e6770f 100755 --- a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh @@ -10,7 +10,7 @@ set -e ./check_env_variables.sh # Example: ./get_action_instances.sh -# https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/actions/README.md#get-apiaction_find-find-actions +# https://github.com/elastic/kibana/blob/master/x-pack/plugins/actions/README.md#get-apiaction_find-find-actions curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/action/_getAll \ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh index 8d8cbdd70a803..c587e9a204182 100755 --- a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh @@ -10,7 +10,7 @@ set -e ./check_env_variables.sh # Example: ./get_action_types.sh -# https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/actions/README.md +# https://github.com/elastic/kibana/blob/master/x-pack/plugins/actions/README.md curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/action/types \ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh index a56d788d69c16..22b602f935187 100755 --- a/x-pack/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh @@ -9,9 +9,15 @@ set -e ./check_env_variables.sh +# Clean up and remove all actions and alerts from SIEM +# within saved objects ./delete_all_actions.sh ./delete_all_alerts.sh ./delete_all_alert_tasks.sh + +# delete all the statuses from the signal index ./delete_all_statuses.sh + +# re-create the signal index ./delete_signal_index.sh ./post_signal_index.sh diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index d298f1cc7cbc6..a8cc6dc680410 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flow, set, omit } from 'lodash/fp'; +import { flow, omit } from 'lodash/fp'; +import set from 'set-value'; import { SearchResponse } from 'elasticsearch'; import { Logger } from '../../../../../../../src/core/server'; @@ -55,8 +56,11 @@ export const transformAnomalyFieldsToEcs = (anomaly: Anomaly): EcsAnomaly => { } const omitDottedFields = omit(errantFields.map(field => field.name)); - const setNestedFields = errantFields.map(field => set(field.name, field.value)); - const setTimestamp = set('@timestamp', new Date(timestamp).toISOString()); + const setNestedFields = errantFields.map(field => (_anomaly: Anomaly) => + set(_anomaly, field.name, field.value) + ); + const setTimestamp = (_anomaly: Anomaly) => + set(_anomaly, '@timestamp', new Date(timestamp).toISOString()); return flow(omitDottedFields, setNestedFields, setTimestamp)(anomaly); }; diff --git a/x-pack/plugins/siem/server/lib/note/saved_object.ts b/x-pack/plugins/siem/server/lib/note/saved_object.ts index 3eae30625e422..219465f551457 100644 --- a/x-pack/plugins/siem/server/lib/note/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/note/saved_object.ts @@ -15,6 +15,11 @@ import { identity } from 'fp-ts/lib/function'; import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; import { AuthenticatedUser } from '../../../../security/common/model'; import { UNAUTHENTICATED_USER } from '../../../common/constants'; +import { + SavedNote, + NoteSavedObjectRuntimeType, + NoteSavedObject, +} from '../../../common/types/timeline/note'; import { PageInfoNote, ResponseNote, @@ -23,178 +28,198 @@ import { NoteResult, } from '../../graphql/types'; import { FrameworkRequest } from '../framework'; -import { SavedNote, NoteSavedObjectRuntimeType, NoteSavedObject } from './types'; import { noteSavedObjectType } from './saved_object_mappings'; import { pickSavedTimeline } from '../timeline/pick_saved_timeline'; import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline'; import { timelineSavedObjectType } from '../timeline/saved_object_mappings'; -export class Note { - public async deleteNote(request: FrameworkRequest, noteIds: string[]) { - const savedObjectsClient = request.context.core.savedObjects.client; - - await Promise.all( - noteIds.map(noteId => savedObjectsClient.delete(noteSavedObjectType, noteId)) - ); - } - - public async deleteNoteByTimelineId(request: FrameworkRequest, timelineId: string) { - const options: SavedObjectsFindOptions = { - type: noteSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - const notesToBeDeleted = await this.getAllSavedNote(request, options); - const savedObjectsClient = request.context.core.savedObjects.client; - - await Promise.all( - notesToBeDeleted.notes.map(note => - savedObjectsClient.delete(noteSavedObjectType, note.noteId) - ) - ); - } - - public async getNote(request: FrameworkRequest, noteId: string): Promise { - return this.getSavedNote(request, noteId); - } - - public async getNotesByEventId( - request: FrameworkRequest, - eventId: string - ): Promise { - const options: SavedObjectsFindOptions = { - type: noteSavedObjectType, - search: eventId, - searchFields: ['eventId'], - }; - const notesByEventId = await this.getAllSavedNote(request, options); - return notesByEventId.notes; - } - - public async getNotesByTimelineId( - request: FrameworkRequest, - timelineId: string - ): Promise { - const options: SavedObjectsFindOptions = { - type: noteSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - const notesByTimelineId = await this.getAllSavedNote(request, options); - return notesByTimelineId.notes; - } - - public async getAllNotes( +export interface Note { + deleteNote: (request: FrameworkRequest, noteIds: string[]) => Promise; + deleteNoteByTimelineId: (request: FrameworkRequest, noteIds: string) => Promise; + getNote: (request: FrameworkRequest, noteId: string) => Promise; + getNotesByEventId: (request: FrameworkRequest, noteId: string) => Promise; + getNotesByTimelineId: (request: FrameworkRequest, noteId: string) => Promise; + getAllNotes: ( request: FrameworkRequest, pageInfo: PageInfoNote | null, search: string | null, sort: SortNote | null - ): Promise { - const options: SavedObjectsFindOptions = { - type: noteSavedObjectType, - perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex : undefined, - search: search != null ? search : undefined, - searchFields: ['note'], - sortField: sort != null ? sort.sortField : undefined, - sortOrder: sort != null ? sort.sortOrder : undefined, - }; - return this.getAllSavedNote(request, options); - } - - public async persistNote( + ) => Promise; + persistNote: ( request: FrameworkRequest, noteId: string | null, version: string | null, note: SavedNote - ): Promise { - try { - const savedObjectsClient = request.context.core.savedObjects.client; - - if (noteId == null) { - const timelineVersionSavedObject = - note.timelineId == null - ? await (async () => { - const timelineResult = convertSavedObjectToSavedTimeline( - await savedObjectsClient.create( - timelineSavedObjectType, - pickSavedTimeline(null, {}, request.user) - ) - ); - note.timelineId = timelineResult.savedObjectId; - return timelineResult.version; - })() - : null; - - // Create new note - return { - code: 200, - message: 'success', - note: convertSavedObjectToSavedNote( - await savedObjectsClient.create( - noteSavedObjectType, - pickSavedNote(noteId, note, request.user) - ), - timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined - ), - }; - } + ) => Promise; + convertSavedObjectToSavedNote: ( + savedObject: unknown, + timelineVersion?: string | undefined | null + ) => NoteSavedObject; +} + +export const deleteNote = async (request: FrameworkRequest, noteIds: string[]) => { + const savedObjectsClient = request.context.core.savedObjects.client; + + await Promise.all(noteIds.map(noteId => savedObjectsClient.delete(noteSavedObjectType, noteId))); +}; + +export const deleteNoteByTimelineId = async (request: FrameworkRequest, timelineId: string) => { + const options: SavedObjectsFindOptions = { + type: noteSavedObjectType, + search: timelineId, + searchFields: ['timelineId'], + }; + const notesToBeDeleted = await getAllSavedNote(request, options); + const savedObjectsClient = request.context.core.savedObjects.client; + + await Promise.all( + notesToBeDeleted.notes.map(note => savedObjectsClient.delete(noteSavedObjectType, note.noteId)) + ); +}; + +export const getNote = async ( + request: FrameworkRequest, + noteId: string +): Promise => { + return getSavedNote(request, noteId); +}; + +export const getNotesByEventId = async ( + request: FrameworkRequest, + eventId: string +): Promise => { + const options: SavedObjectsFindOptions = { + type: noteSavedObjectType, + search: eventId, + searchFields: ['eventId'], + }; + const notesByEventId = await getAllSavedNote(request, options); + return notesByEventId.notes; +}; + +export const getNotesByTimelineId = async ( + request: FrameworkRequest, + timelineId: string +): Promise => { + const options: SavedObjectsFindOptions = { + type: noteSavedObjectType, + search: timelineId, + searchFields: ['timelineId'], + }; + const notesByTimelineId = await getAllSavedNote(request, options); + return notesByTimelineId.notes; +}; + +export const getAllNotes = async ( + request: FrameworkRequest, + pageInfo: PageInfoNote | null, + search: string | null, + sort: SortNote | null +): Promise => { + const options: SavedObjectsFindOptions = { + type: noteSavedObjectType, + perPage: pageInfo != null ? pageInfo.pageSize : undefined, + page: pageInfo != null ? pageInfo.pageIndex : undefined, + search: search != null ? search : undefined, + searchFields: ['note'], + sortField: sort != null ? sort.sortField : undefined, + sortOrder: sort != null ? sort.sortOrder : undefined, + }; + return getAllSavedNote(request, options); +}; + +export const persistNote = async ( + request: FrameworkRequest, + noteId: string | null, + version: string | null, + note: SavedNote +): Promise => { + try { + const savedObjectsClient = request.context.core.savedObjects.client; - // Update new note + if (noteId == null) { + const timelineVersionSavedObject = + note.timelineId == null + ? await (async () => { + const timelineResult = convertSavedObjectToSavedTimeline( + await savedObjectsClient.create( + timelineSavedObjectType, + pickSavedTimeline(null, {}, request.user) + ) + ); + note.timelineId = timelineResult.savedObjectId; + return timelineResult.version; + })() + : null; - const existingNote = await this.getSavedNote(request, noteId); + // Create new note return { code: 200, message: 'success', note: convertSavedObjectToSavedNote( - await savedObjectsClient.update( + await savedObjectsClient.create( noteSavedObjectType, - noteId, - pickSavedNote(noteId, note, request.user), - { - version: existingNote.version || undefined, - } - ) + pickSavedNote(noteId, note, request.user) + ), + timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined ), }; - } catch (err) { - if (getOr(null, 'output.statusCode', err) === 403) { - const noteToReturn: NoteResult = { - ...note, - noteId: uuid.v1(), - version: '', - timelineId: '', - timelineVersion: '', - }; - return { - code: 403, - message: err.message, - note: noteToReturn, - }; - } - throw err; } - } - private async getSavedNote(request: FrameworkRequest, NoteId: string) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(noteSavedObjectType, NoteId); - - return convertSavedObjectToSavedNote(savedObject); - } - - private async getAllSavedNote(request: FrameworkRequest, options: SavedObjectsFindOptions) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObjects = await savedObjectsClient.find(options); + // Update new note + const existingNote = await getSavedNote(request, noteId); return { - totalCount: savedObjects.total, - notes: savedObjects.saved_objects.map(savedObject => - convertSavedObjectToSavedNote(savedObject) + code: 200, + message: 'success', + note: convertSavedObjectToSavedNote( + await savedObjectsClient.update( + noteSavedObjectType, + noteId, + pickSavedNote(noteId, note, request.user), + { + version: existingNote.version || undefined, + } + ) ), }; + } catch (err) { + if (getOr(null, 'output.statusCode', err) === 403) { + const noteToReturn: NoteResult = { + ...note, + noteId: uuid.v1(), + version: '', + timelineId: '', + timelineVersion: '', + }; + return { + code: 403, + message: err.message, + note: noteToReturn, + }; + } + throw err; } -} +}; + +const getSavedNote = async (request: FrameworkRequest, NoteId: string) => { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObject = await savedObjectsClient.get(noteSavedObjectType, NoteId); + + return convertSavedObjectToSavedNote(savedObject); +}; + +const getAllSavedNote = async (request: FrameworkRequest, options: SavedObjectsFindOptions) => { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObjects = await savedObjectsClient.find(options); + + return { + totalCount: savedObjects.total, + notes: savedObjects.saved_objects.map(savedObject => + convertSavedObjectToSavedNote(savedObject) + ), + }; +}; export const convertSavedObjectToSavedNote = ( savedObject: unknown, diff --git a/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts b/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts index 1e3a481e17106..c653f23d5c149 100644 --- a/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts @@ -13,174 +13,224 @@ import { identity } from 'fp-ts/lib/function'; import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; import { AuthenticatedUser } from '../../../../security/common/model'; import { UNAUTHENTICATED_USER } from '../../../common/constants'; -import { FrameworkRequest } from '../framework'; import { PinnedEventSavedObject, PinnedEventSavedObjectRuntimeType, SavedPinnedEvent, -} from './types'; +} from '../../../common/types/timeline/pinned_event'; +import { FrameworkRequest } from '../framework'; + import { PageInfoNote, SortNote, PinnedEvent as PinnedEventResponse } from '../../graphql/types'; import { pickSavedTimeline } from '../timeline/pick_saved_timeline'; import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline'; import { pinnedEventSavedObjectType } from './saved_object_mappings'; import { timelineSavedObjectType } from '../timeline/saved_object_mappings'; -export class PinnedEvent { - public async deletePinnedEventOnTimeline(request: FrameworkRequest, pinnedEventIds: string[]) { - const savedObjectsClient = request.context.core.savedObjects.client; - - await Promise.all( - pinnedEventIds.map(pinnedEventId => - savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEventId) - ) - ); - } +export interface PinnedEvent { + deletePinnedEventOnTimeline: ( + request: FrameworkRequest, + pinnedEventIds: string[] + ) => Promise; - public async deleteAllPinnedEventsOnTimeline(request: FrameworkRequest, timelineId: string) { - const savedObjectsClient = request.context.core.savedObjects.client; - const options: SavedObjectsFindOptions = { - type: pinnedEventSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - const pinnedEventToBeDeleted = await this.getAllSavedPinnedEvents(request, options); - await Promise.all( - pinnedEventToBeDeleted.map(pinnedEvent => - savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEvent.pinnedEventId) - ) - ); - } + deleteAllPinnedEventsOnTimeline: (request: FrameworkRequest, timelineId: string) => Promise; - public async getPinnedEvent( + getPinnedEvent: ( request: FrameworkRequest, pinnedEventId: string - ): Promise { - return this.getSavedPinnedEvent(request, pinnedEventId); - } + ) => Promise; - public async getAllPinnedEventsByTimelineId( + getAllPinnedEventsByTimelineId: ( request: FrameworkRequest, timelineId: string - ): Promise { - const options: SavedObjectsFindOptions = { - type: pinnedEventSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - return this.getAllSavedPinnedEvents(request, options); - } + ) => Promise; - public async getAllPinnedEvents( + getAllPinnedEvents: ( request: FrameworkRequest, pageInfo: PageInfoNote | null, search: string | null, sort: SortNote | null - ): Promise { - const options: SavedObjectsFindOptions = { - type: pinnedEventSavedObjectType, - perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex : undefined, - search: search != null ? search : undefined, - searchFields: ['timelineId', 'eventId'], - sortField: sort != null ? sort.sortField : undefined, - sortOrder: sort != null ? sort.sortOrder : undefined, - }; - return this.getAllSavedPinnedEvents(request, options); - } + ) => Promise; - public async persistPinnedEventOnTimeline( + persistPinnedEventOnTimeline: ( request: FrameworkRequest, pinnedEventId: string | null, // pinned event saved object id eventId: string, timelineId: string | null - ): Promise { - const savedObjectsClient = request.context.core.savedObjects.client; - - try { - if (pinnedEventId == null) { - const timelineVersionSavedObject = - timelineId == null - ? await (async () => { - const timelineResult = convertSavedObjectToSavedTimeline( - await savedObjectsClient.create( - timelineSavedObjectType, - pickSavedTimeline(null, {}, request.user || null) - ) - ); - timelineId = timelineResult.savedObjectId; // eslint-disable-line no-param-reassign - return timelineResult.version; - })() - : null; - - if (timelineId != null) { - const allPinnedEventId = await this.getAllPinnedEventsByTimelineId(request, timelineId); - const isPinnedAlreadyExisting = allPinnedEventId.filter( - pinnedEvent => pinnedEvent.eventId === eventId - ); + ) => Promise; - if (isPinnedAlreadyExisting.length === 0) { - const savedPinnedEvent: SavedPinnedEvent = { - eventId, - timelineId, - }; - // create Pinned Event on Timeline - return convertSavedObjectToSavedPinnedEvent( - await savedObjectsClient.create( - pinnedEventSavedObjectType, - pickSavedPinnedEvent(pinnedEventId, savedPinnedEvent, request.user || null) - ), - timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined - ); - } - return isPinnedAlreadyExisting[0]; + convertSavedObjectToSavedPinnedEvent: ( + savedObject: unknown, + timelineVersion?: string | undefined | null + ) => PinnedEventSavedObject; + + pickSavedPinnedEvent: ( + pinnedEventId: string | null, + savedPinnedEvent: SavedPinnedEvent, + userInfo: AuthenticatedUser | null + ) => // eslint-disable-next-line @typescript-eslint/no-explicit-any + any; +} + +export const deletePinnedEventOnTimeline = async ( + request: FrameworkRequest, + pinnedEventIds: string[] +) => { + const savedObjectsClient = request.context.core.savedObjects.client; + + await Promise.all( + pinnedEventIds.map(pinnedEventId => + savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEventId) + ) + ); +}; + +export const deleteAllPinnedEventsOnTimeline = async ( + request: FrameworkRequest, + timelineId: string +) => { + const savedObjectsClient = request.context.core.savedObjects.client; + const options: SavedObjectsFindOptions = { + type: pinnedEventSavedObjectType, + search: timelineId, + searchFields: ['timelineId'], + }; + const pinnedEventToBeDeleted = await getAllSavedPinnedEvents(request, options); + await Promise.all( + pinnedEventToBeDeleted.map(pinnedEvent => + savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEvent.pinnedEventId) + ) + ); +}; + +export const getPinnedEvent = async ( + request: FrameworkRequest, + pinnedEventId: string +): Promise => { + return getSavedPinnedEvent(request, pinnedEventId); +}; + +export const getAllPinnedEventsByTimelineId = async ( + request: FrameworkRequest, + timelineId: string +): Promise => { + const options: SavedObjectsFindOptions = { + type: pinnedEventSavedObjectType, + search: timelineId, + searchFields: ['timelineId'], + }; + return getAllSavedPinnedEvents(request, options); +}; + +export const getAllPinnedEvents = async ( + request: FrameworkRequest, + pageInfo: PageInfoNote | null, + search: string | null, + sort: SortNote | null +): Promise => { + const options: SavedObjectsFindOptions = { + type: pinnedEventSavedObjectType, + perPage: pageInfo != null ? pageInfo.pageSize : undefined, + page: pageInfo != null ? pageInfo.pageIndex : undefined, + search: search != null ? search : undefined, + searchFields: ['timelineId', 'eventId'], + sortField: sort != null ? sort.sortField : undefined, + sortOrder: sort != null ? sort.sortOrder : undefined, + }; + return getAllSavedPinnedEvents(request, options); +}; + +export const persistPinnedEventOnTimeline = async ( + request: FrameworkRequest, + pinnedEventId: string | null, // pinned event saved object id + eventId: string, + timelineId: string | null +): Promise => { + const savedObjectsClient = request.context.core.savedObjects.client; + + try { + if (pinnedEventId == null) { + const timelineVersionSavedObject = + timelineId == null + ? await (async () => { + const timelineResult = convertSavedObjectToSavedTimeline( + await savedObjectsClient.create( + timelineSavedObjectType, + pickSavedTimeline(null, {}, request.user || null) + ) + ); + timelineId = timelineResult.savedObjectId; // eslint-disable-line no-param-reassign + return timelineResult.version; + })() + : null; + + if (timelineId != null) { + const allPinnedEventId = await getAllPinnedEventsByTimelineId(request, timelineId); + const isPinnedAlreadyExisting = allPinnedEventId.filter( + pinnedEvent => pinnedEvent.eventId === eventId + ); + + if (isPinnedAlreadyExisting.length === 0) { + const savedPinnedEvent: SavedPinnedEvent = { + eventId, + timelineId, + }; + // create Pinned Event on Timeline + return convertSavedObjectToSavedPinnedEvent( + await savedObjectsClient.create( + pinnedEventSavedObjectType, + pickSavedPinnedEvent(pinnedEventId, savedPinnedEvent, request.user || null) + ), + timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined + ); } - throw new Error('You can NOT pinned event without a timelineID'); + return isPinnedAlreadyExisting[0]; } - // Delete Pinned Event on Timeline - await this.deletePinnedEventOnTimeline(request, [pinnedEventId]); + throw new Error('You can NOT pinned event without a timelineID'); + } + // Delete Pinned Event on Timeline + await deletePinnedEventOnTimeline(request, [pinnedEventId]); + return null; + } catch (err) { + if (getOr(null, 'output.statusCode', err) === 404) { + /* + * Why we are doing that, because if it is not found for sure that it will be unpinned + * There is no need to bring back this error since we can assume that it is unpinned + */ return null; - } catch (err) { - if (getOr(null, 'output.statusCode', err) === 404) { - /* - * Why we are doing that, because if it is not found for sure that it will be unpinned - * There is no need to bring back this error since we can assume that it is unpinned - */ - return null; - } - if (getOr(null, 'output.statusCode', err) === 403) { - return pinnedEventId != null - ? { - code: 403, - message: err.message, - pinnedEventId: eventId, - timelineId: '', - timelineVersion: '', - } - : null; - } - throw err; } + if (getOr(null, 'output.statusCode', err) === 403) { + return pinnedEventId != null + ? { + code: 403, + message: err.message, + pinnedEventId: eventId, + timelineId: '', + timelineVersion: '', + } + : null; + } + throw err; } +}; - private async getSavedPinnedEvent(request: FrameworkRequest, pinnedEventId: string) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(pinnedEventSavedObjectType, pinnedEventId); +const getSavedPinnedEvent = async (request: FrameworkRequest, pinnedEventId: string) => { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObject = await savedObjectsClient.get(pinnedEventSavedObjectType, pinnedEventId); - return convertSavedObjectToSavedPinnedEvent(savedObject); - } + return convertSavedObjectToSavedPinnedEvent(savedObject); +}; - private async getAllSavedPinnedEvents( - request: FrameworkRequest, - options: SavedObjectsFindOptions - ) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObjects = await savedObjectsClient.find(options); - - return savedObjects.saved_objects.map(savedObject => - convertSavedObjectToSavedPinnedEvent(savedObject) - ); - } -} +const getAllSavedPinnedEvents = async ( + request: FrameworkRequest, + options: SavedObjectsFindOptions +) => { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObjects = await savedObjectsClient.find(options); + + return savedObjects.saved_objects.map(savedObject => + convertSavedObjectToSavedPinnedEvent(savedObject) + ); +}; export const convertSavedObjectToSavedPinnedEvent = ( savedObject: unknown, diff --git a/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts b/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts index ea5db565483c8..bde24a338ec84 100644 --- a/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts +++ b/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts @@ -8,16 +8,21 @@ import { failure } from 'io-ts/lib/PathReporter'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { TimelineSavedObjectRuntimeType, TimelineSavedObject } from './types'; +import { + TimelineSavedObjectRuntimeType, + TimelineSavedObject, +} from '../../../common/types/timeline'; export const convertSavedObjectToSavedTimeline = (savedObject: unknown): TimelineSavedObject => { const timeline = pipe( TimelineSavedObjectRuntimeType.decode(savedObject), - map(savedTimeline => ({ - savedObjectId: savedTimeline.id, - version: savedTimeline.version, - ...savedTimeline.attributes, - })), + map(savedTimeline => { + return { + savedObjectId: savedTimeline.id, + version: savedTimeline.version, + ...savedTimeline.attributes, + }; + }), fold(errors => { throw new Error(failure(errors).join('\n')); }, identity) diff --git a/x-pack/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts b/x-pack/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts index abe8de9bf5b94..6b4017b5e4d5c 100644 --- a/x-pack/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts +++ b/x-pack/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts @@ -22,6 +22,7 @@ import { import { ImportTimelineResponse } from './routes/utils/import_timelines'; import { ImportTimelinesSchemaRt } from './routes/schemas/import_timelines_schema'; +import { BadRequestError } from '../detection_engine/errors/bad_request_error'; type ErrorFactory = (message: string) => Error; @@ -38,8 +39,11 @@ export const decodeOrThrow = ( pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity)); export const validateTimelines = (): Transform => - createMapStream((obj: ImportTimelineResponse) => decodeOrThrow(ImportTimelinesSchemaRt)(obj)); - + createMapStream((obj: ImportTimelineResponse) => + obj instanceof Error + ? new BadRequestError(obj.message) + : decodeOrThrow(ImportTimelinesSchemaRt)(obj) + ); export const createTimelinesStreamFromNdJson = (ruleLimit: number) => { return [ createSplitStream('\n'), diff --git a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts index 19adb7ac1045a..eeded1cc2532d 100644 --- a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts +++ b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import uuid from 'uuid'; import { AuthenticatedUser } from '../../../../security/common/model'; import { UNAUTHENTICATED_USER } from '../../../common/constants'; -import { SavedTimeline } from './types'; +import { SavedTimeline, TimelineType } from '../../../common/types/timeline'; export const pickSavedTimeline = ( timelineId: string | null, @@ -24,5 +25,21 @@ export const pickSavedTimeline = ( savedTimeline.updated = dateNow; savedTimeline.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; } + + if (savedTimeline.timelineType === TimelineType.template) { + savedTimeline.timelineType = TimelineType.template; + if (savedTimeline.templateTimelineId == null) { + savedTimeline.templateTimelineId = uuid.v4(); + } + + if (savedTimeline.templateTimelineVersion == null) { + savedTimeline.templateTimelineVersion = 1; + } + } else { + savedTimeline.timelineType = TimelineType.default; + savedTimeline.templateTimelineId = null; + savedTimeline.templateTimelineVersion = null; + } + return savedTimeline; }; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/README.md b/x-pack/plugins/siem/server/lib/timeline/routes/README.md new file mode 100644 index 0000000000000..2c5547e39fc4e --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/README.md @@ -0,0 +1,326 @@ +**Timeline apis** + + 1. Create timeline api + 2. Update timeline api + 3. Create template timeline api + 4. Update template timeline api + + +## Create timeline api +#### POST /api/timeline +##### Authorization +Type: Basic Auth +username: Your Kibana username +password: Your Kibana password + + +##### Request header +``` +Content-Type: application/json +kbn-version: 8.0.0 +``` +##### Request body +```json +{ + "timeline": { + "columns": [ + { + "columnHeaderType": "not-filtered", + "id": "@timestamp" + }, + { + "columnHeaderType": "not-filtered", + "id": "message" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.category" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.action" + }, + { + "columnHeaderType": "not-filtered", + "id": "host.name" + }, + { + "columnHeaderType": "not-filtered", + "id": "source.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "destination.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "user.name" + } + ], + "dataProviders": [], + "description": "", + "eventType": "all", + "filters": [], + "kqlMode": "filter", + "kqlQuery": { + "filterQuery": null + }, + "title": "abd", + "dateRange": { + "start": 1587370079200, + "end": 1587456479201 + }, + "savedQueryId": null, + "sort": { + "columnId": "@timestamp", + "sortDirection": "desc" + } + }, + "timelineId":null, // Leave this as null + "version":null // Leave this as null +} +``` + + +## Update timeline api +#### PATCH /api/timeline +##### Authorization +Type: Basic Auth +username: Your Kibana username +password: Your Kibana password + + +##### Request header +``` +Content-Type: application/json +kbn-version: 8.0.0 +``` +##### Request body +```json +{ + "timeline": { + "columns": [ + { + "columnHeaderType": "not-filtered", + "id": "@timestamp" + }, + { + "columnHeaderType": "not-filtered", + "id": "message" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.category" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.action" + }, + { + "columnHeaderType": "not-filtered", + "id": "host.name" + }, + { + "columnHeaderType": "not-filtered", + "id": "source.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "destination.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "user.name" + } + ], + "dataProviders": [], + "description": "", + "eventType": "all", + "filters": [], + "kqlMode": "filter", + "kqlQuery": { + "filterQuery": null + }, + "title": "abd", + "dateRange": { + "start": 1587370079200, + "end": 1587456479201 + }, + "savedQueryId": null, + "sort": { + "columnId": "@timestamp", + "sortDirection": "desc" + }, + "created": 1587468588922, + "createdBy": "casetester", + "updated": 1587468588922, + "updatedBy": "casetester", + "timelineType": "default" + }, + "timelineId":"68ea5330-83c3-11ea-bff9-ab01dd7cb6cc", // Have to match the existing timeline savedObject id + "version":"WzYwLDFd" // Have to match the existing timeline version +} +``` + +## Create template timeline api +#### POST /api/timeline +##### Authorization +Type: Basic Auth +username: Your Kibana username +password: Your Kibana password + + +##### Request header +``` +Content-Type: application/json +kbn-version: 8.0.0 +``` +##### Request body +```json +{ + "timeline": { + "columns": [ + { + "columnHeaderType": "not-filtered", + "id": "@timestamp" + }, + { + "columnHeaderType": "not-filtered", + "id": "message" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.category" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.action" + }, + { + "columnHeaderType": "not-filtered", + "id": "host.name" + }, + { + "columnHeaderType": "not-filtered", + "id": "source.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "destination.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "user.name" + } + ], + "dataProviders": [ + + ], + "description": "", + "eventType": "all", + "filters": [ + + ], + "kqlMode": "filter", + "kqlQuery": { + "filterQuery": null + }, + "title": "abd", + "dateRange": { + "start": 1587370079200, + "end": 1587456479201 + }, + "savedQueryId": null, + "sort": { + "columnId": "@timestamp", + "sortDirection": "desc" + }, + "timelineType": "template" // This is the difference between create timeline + }, + "timelineId":null, // Leave this as null + "version":null // Leave this as null +} +``` + + +## Update template timeline api +#### PATCH /api/timeline +##### Authorization +Type: Basic Auth +username: Your Kibana username +password: Your Kibana password + + +##### Request header +``` +Content-Type: application/json +kbn-version: 8.0.0 +``` +##### Request body +```json +{ + "timeline": { + "columns": [ + { + "columnHeaderType": "not-filtered", + "id": "@timestamp" + }, + { + "columnHeaderType": "not-filtered", + "id": "message" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.category" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.action" + }, + { + "columnHeaderType": "not-filtered", + "id": "host.name" + }, + { + "columnHeaderType": "not-filtered", + "id": "source.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "destination.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "user.name" + } + ], + "dataProviders": [], + "description": "", + "eventType": "all", + "filters": [], + "kqlMode": "filter", + "kqlQuery": { + "filterQuery": null + }, + "title": "abd", + "dateRange": { + "start": 1587370079200, + "end": 1587456479201 + }, + "savedQueryId": null, + "sort": { + "columnId": "@timestamp", + "sortDirection": "desc" + }, + "timelineType": "template", + "created": 1587473119992, + "createdBy": "casetester", + "updated": 1587473119992, + "updatedBy": "casetester", + "templateTimelineId": "745d0316-6af7-43bf-afd6-9747119754fb", // Please provide the existing template timeline version + "templateTimelineVersion": 2 // Please provide a template timeline version grater than existing one + }, + "timelineId":"f5a4bd10-83cd-11ea-bf78-0547a65f1281", // This is a must as well + "version":"Wzg2LDFd" // Please provide the existing timeline version +} +``` \ No newline at end of file diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts index 686f2b491cf88..a832c818d48b0 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -5,6 +5,7 @@ */ import { omit } from 'lodash/fp'; +import { TimelineType } from '../../../../../common/types/timeline'; export const mockDuplicateIdErrors = []; @@ -148,6 +149,13 @@ export const mockGetTimelineValue = { pinnedEventIds: ['k-gi8nABm-sIqJ_scOoS'], }; +export const mockGetTemplateTimelineValue = { + ...mockGetTimelineValue, + timelineType: TimelineType.template, + templateTimelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + templateTimelineVersion: 1, +}; + export const mockParsedTimelineObject = omit( [ 'globalNotes', diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts index a83c443773302..304ca309775ff 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -3,10 +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 { TIMELINE_EXPORT_URL, TIMELINE_IMPORT_URL } from '../../../../../common/constants'; -import { requestMock } from '../../../detection_engine/routes/__mocks__'; +import * as rt from 'io-ts'; +import { + TIMELINE_EXPORT_URL, + TIMELINE_IMPORT_URL, + TIMELINE_URL, +} from '../../../../../common/constants'; import stream from 'stream'; +import { requestMock } from '../../../detection_engine/routes/__mocks__'; +import { SavedTimeline, TimelineType } from '../../../../../common/types/timeline'; +import { updateTimelineSchema } from '../schemas/update_timelines_schema'; +import { createTimelineSchema } from '../schemas/create_timelines_schema'; + const readable = new stream.Readable(); export const getExportTimelinesRequest = () => requestMock.create({ @@ -31,6 +39,96 @@ export const getImportTimelinesRequest = (filename?: string) => }, }); +export const inputTimeline: SavedTimeline = { + columns: [ + { columnHeaderType: 'not-filtered', id: '@timestamp' }, + { columnHeaderType: 'not-filtered', id: 'message' }, + { columnHeaderType: 'not-filtered', id: 'event.category' }, + { columnHeaderType: 'not-filtered', id: 'event.action' }, + { columnHeaderType: 'not-filtered', id: 'host.name' }, + { columnHeaderType: 'not-filtered', id: 'source.ip' }, + { columnHeaderType: 'not-filtered', id: 'destination.ip' }, + { columnHeaderType: 'not-filtered', id: 'user.name' }, + ], + dataProviders: [], + description: '', + eventType: 'all', + filters: [], + kqlMode: 'filter', + kqlQuery: { filterQuery: null }, + title: 't', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, + dateRange: { start: 1585227005527, end: 1585313405527 }, + savedQueryId: null, + sort: { columnId: '@timestamp', sortDirection: 'desc' }, +}; + +export const inputTemplateTimeline = { + ...inputTimeline, + timelineType: TimelineType.template, + templateTimelineId: null, + templateTimelineVersion: null, +}; + +export const createTimelineWithoutTimelineId = { + templateTimelineId: null, + timeline: inputTimeline, + timelineId: null, + version: null, + timelineType: TimelineType.default, +}; + +export const createTemplateTimelineWithoutTimelineId = { + templateTimelineId: null, + timeline: inputTemplateTimeline, + timelineId: null, + version: null, + timelineType: TimelineType.template, +}; + +export const createTimelineWithTimelineId = { + ...createTimelineWithoutTimelineId, + timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', +}; + +export const createTemplateTimelineWithTimelineId = { + ...createTemplateTimelineWithoutTimelineId, + timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + templateTimelineId: 'existing template timeline id', +}; + +export const updateTimelineWithTimelineId = { + timeline: inputTimeline, + timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + version: 'WzEyMjUsMV0=', +}; + +export const updateTemplateTimelineWithTimelineId = { + timeline: { + ...inputTemplateTimeline, + templateTimelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + templateTimelineVersion: 2, + }, + timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + version: 'WzEyMjUsMV0=', +}; + +export const getCreateTimelinesRequest = (mockBody: rt.TypeOf) => + requestMock.create({ + method: 'post', + path: TIMELINE_URL, + body: mockBody, + }); + +export const getUpdateTimelinesRequest = (mockBody: rt.TypeOf) => + requestMock.create({ + method: 'patch', + path: TIMELINE_URL, + body: mockBody, + }); + export const getImportTimelinesRequestEnableOverwrite = (filename?: string) => requestMock.create({ method: 'post', diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.test.ts new file mode 100644 index 0000000000000..70ee1532395a5 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.test.ts @@ -0,0 +1,272 @@ +/* + * 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 { SecurityPluginSetup } from '../../../../../../plugins/security/server'; + +import { + serverMock, + requestContextMock, + createMockConfig, +} from '../../detection_engine/routes/__mocks__'; + +import { + mockGetCurrentUser, + mockGetTimelineValue, + mockGetTemplateTimelineValue, +} from './__mocks__/import_timelines'; +import { + getCreateTimelinesRequest, + inputTimeline, + createTimelineWithoutTimelineId, + createTimelineWithTimelineId, + createTemplateTimelineWithoutTimelineId, + createTemplateTimelineWithTimelineId, +} from './__mocks__/request_responses'; +import { + CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + CREATE_TIMELINE_ERROR_MESSAGE, +} from './utils/create_timelines'; + +describe('create timelines', () => { + let server: ReturnType; + let securitySetup: SecurityPluginSetup; + let { context } = requestContextMock.createTools(); + let mockGetTimeline: jest.Mock; + let mockPersistTimeline: jest.Mock; + let mockPersistPinnedEventOnTimeline: jest.Mock; + let mockPersistNote: jest.Mock; + + beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + + server = serverMock.create(); + context = requestContextMock.createTools().context; + + securitySetup = ({ + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown) as SecurityPluginSetup; + + mockGetTimeline = jest.fn(); + mockPersistTimeline = jest.fn(); + mockPersistPinnedEventOnTimeline = jest.fn(); + mockPersistNote = jest.fn(); + }); + + describe('Manipulate timeline', () => { + describe('Create a new timeline', () => { + beforeEach(async () => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(null), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: createTimelineWithTimelineId, + }), + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const createTimelinesRoute = jest.requireActual('./create_timelines_route') + .createTimelinesRoute; + createTimelinesRoute(server.router, createMockConfig(), securitySetup); + + const mockRequest = getCreateTimelinesRequest(createTimelineWithoutTimelineId); + await server.inject(mockRequest, context); + }); + + test('should Create a new timeline savedObject', async () => { + expect(mockPersistTimeline).toHaveBeenCalled(); + }); + + test('should Create a new timeline savedObject without timelineId', async () => { + expect(mockPersistTimeline.mock.calls[0][1]).toBeNull(); + }); + + test('should Create a new timeline savedObject without timeline version', async () => { + expect(mockPersistTimeline.mock.calls[0][2]).toBeNull(); + }); + + test('should Create a new timeline savedObject witn given timeline', async () => { + expect(mockPersistTimeline.mock.calls[0][3]).toEqual(inputTimeline); + }); + + test('should NOT Create new pinned events', async () => { + expect(mockPersistPinnedEventOnTimeline).not.toBeCalled(); + }); + + test('should NOT Create notes', async () => { + expect(mockPersistNote).not.toBeCalled(); + }); + + test('returns 200 when create timeline successfully', async () => { + const response = await server.inject( + getCreateTimelinesRequest(createTimelineWithoutTimelineId), + context + ); + expect(response.status).toEqual(200); + }); + }); + + describe('Import a timeline already exist', () => { + beforeEach(() => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), + persistTimeline: mockPersistTimeline, + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const createTimelinesRoute = jest.requireActual('./create_timelines_route') + .createTimelinesRoute; + createTimelinesRoute(server.router, createMockConfig(), securitySetup); + }); + + test('returns error message', async () => { + const response = await server.inject( + getCreateTimelinesRequest(createTimelineWithTimelineId), + context + ); + expect(response.body).toEqual({ + message: CREATE_TIMELINE_ERROR_MESSAGE, + status_code: 405, + }); + }); + }); + }); + + describe('Manipulate template timeline', () => { + describe('Create a new template timeline', () => { + beforeEach(async () => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(null), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: createTemplateTimelineWithTimelineId, + }), + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const createTimelinesRoute = jest.requireActual('./create_timelines_route') + .createTimelinesRoute; + createTimelinesRoute(server.router, createMockConfig(), securitySetup); + + const mockRequest = getCreateTimelinesRequest(createTemplateTimelineWithoutTimelineId); + await server.inject(mockRequest, context); + }); + + test('should Create a new template timeline savedObject', async () => { + expect(mockPersistTimeline).toHaveBeenCalled(); + }); + + test('should Create a new template timeline savedObject without timelineId', async () => { + expect(mockPersistTimeline.mock.calls[0][1]).toBeNull(); + }); + + test('should Create a new template timeline savedObject without template timeline version', async () => { + expect(mockPersistTimeline.mock.calls[0][2]).toBeNull(); + }); + + test('should Create a new template timeline savedObject witn given template timeline', async () => { + expect(mockPersistTimeline.mock.calls[0][3]).toEqual( + createTemplateTimelineWithTimelineId.timeline + ); + }); + + test('should NOT Create new pinned events', async () => { + expect(mockPersistPinnedEventOnTimeline).not.toBeCalled(); + }); + + test('should NOT Create notes', async () => { + expect(mockPersistNote).not.toBeCalled(); + }); + + test('returns 200 when create timeline successfully', async () => { + const response = await server.inject( + getCreateTimelinesRequest(createTimelineWithoutTimelineId), + context + ); + expect(response.status).toEqual(200); + }); + }); + + describe('Import a template timeline already exist', () => { + beforeEach(() => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), + persistTimeline: mockPersistTimeline, + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const createTimelinesRoute = jest.requireActual('./create_timelines_route') + .createTimelinesRoute; + createTimelinesRoute(server.router, createMockConfig(), securitySetup); + }); + + test('returns error message', async () => { + const response = await server.inject( + getCreateTimelinesRequest(createTemplateTimelineWithTimelineId), + context + ); + expect(response.body).toEqual({ + message: CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + status_code: 405, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.ts new file mode 100644 index 0000000000000..c456ae31fb7da --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.ts @@ -0,0 +1,90 @@ +/* + * 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 { IRouter } from '../../../../../../../src/core/server'; + +import { TIMELINE_URL } from '../../../../common/constants'; +import { TimelineType } from '../../../../common/types/timeline'; + +import { ConfigType } from '../../..'; +import { SetupPlugins } from '../../../plugin'; +import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; + +import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; + +import { createTimelineSchema } from './schemas/create_timelines_schema'; +import { buildFrameworkRequest } from './utils/common'; +import { + createTimelines, + getTimeline, + getTemplateTimeline, + CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + CREATE_TIMELINE_ERROR_MESSAGE, +} from './utils/create_timelines'; + +export const createTimelinesRoute = ( + router: IRouter, + config: ConfigType, + security: SetupPlugins['security'] +) => { + router.post( + { + path: TIMELINE_URL, + validate: { + body: buildRouteValidation(createTimelineSchema), + }, + options: { + tags: ['access:siem'], + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const frameworkRequest = await buildFrameworkRequest(context, security, request); + + const { timelineId, timeline, version } = request.body; + const { templateTimelineId, timelineType } = timeline; + const isHandlingTemplateTimeline = timelineType === TimelineType.template; + + const existTimeline = + timelineId != null ? await getTimeline(frameworkRequest, timelineId) : null; + const existTemplateTimeline = + templateTimelineId != null + ? await getTemplateTimeline(frameworkRequest, templateTimelineId) + : null; + + if ( + (!isHandlingTemplateTimeline && existTimeline != null) || + (isHandlingTemplateTimeline && (existTemplateTimeline != null || existTimeline != null)) + ) { + return siemResponse.error({ + body: isHandlingTemplateTimeline + ? CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE + : CREATE_TIMELINE_ERROR_MESSAGE, + statusCode: 405, + }); + } + + // Create timeline + const newTimeline = await createTimelines(frameworkRequest, timeline, null, version); + return response.ok({ + body: { + data: { + persistTimeline: newTimeline, + }, + }, + }); + } catch (err) { + const error = transformError(err); + + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts index 9f41943cfa27f..56c152d02ae98 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts @@ -6,13 +6,13 @@ import { getImportTimelinesRequest } from './__mocks__/request_responses'; import { - createMockConfig, serverMock, requestContextMock, requestMock, + createMockConfig, } from '../../detection_engine/routes/__mocks__'; import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; -import { SecurityPluginSetup } from '../../../../../security/server'; +import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; import { mockUniqueParsedObjects, @@ -24,7 +24,6 @@ import { } from './__mocks__/import_timelines'; describe('import timelines', () => { - let config: ReturnType; let server: ReturnType; let request: ReturnType; let securitySetup: SecurityPluginSetup; @@ -43,7 +42,6 @@ describe('import timelines', () => { server = serverMock.create(); context = requestContextMock.createTools().context; - config = createMockConfig(); securitySetup = ({ authc: { @@ -84,40 +82,28 @@ describe('import timelines', () => { beforeEach(() => { jest.doMock('../saved_object', () => { return { - Timeline: jest.fn().mockImplementation(() => { - return { - getTimeline: mockGetTimeline.mockReturnValue(null), - persistTimeline: mockPersistTimeline.mockReturnValue({ - timeline: { savedObjectId: newTimelineSavedObjectId, version: newTimelineVersion }, - }), - }; + getTimeline: mockGetTimeline.mockReturnValue(null), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: { savedObjectId: newTimelineSavedObjectId, version: newTimelineVersion }, }), }; }); jest.doMock('../../pinned_event/saved_object', () => { return { - PinnedEvent: jest.fn().mockImplementation(() => { - return { - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, - }; - }), + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, }; }); jest.doMock('../../note/saved_object', () => { return { - Note: jest.fn().mockImplementation(() => { - return { - persistNote: mockPersistNote, - }; - }), + persistNote: mockPersistNote, }; }); const importTimelinesRoute = jest.requireActual('./import_timelines_route') .importTimelinesRoute; - importTimelinesRoute(server.router, config, securitySetup); + importTimelinesRoute(server.router, createMockConfig(), securitySetup); }); test('should use given timelineId to check if the timeline savedObject already exist', async () => { @@ -230,38 +216,26 @@ describe('import timelines', () => { beforeEach(() => { jest.doMock('../saved_object', () => { return { - Timeline: jest.fn().mockImplementation(() => { - return { - getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), - persistTimeline: mockPersistTimeline, - }; - }), + getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), + persistTimeline: mockPersistTimeline, }; }); jest.doMock('../../pinned_event/saved_object', () => { return { - PinnedEvent: jest.fn().mockImplementation(() => { - return { - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, - }; - }), + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, }; }); jest.doMock('../../note/saved_object', () => { return { - Note: jest.fn().mockImplementation(() => { - return { - persistNote: mockPersistNote, - }; - }), + persistNote: mockPersistNote, }; }); const importTimelinesRoute = jest.requireActual('./import_timelines_route') .importTimelinesRoute; - importTimelinesRoute(server.router, config, securitySetup); + importTimelinesRoute(server.router, createMockConfig(), securitySetup); }); test('returns error message', async () => { @@ -286,36 +260,24 @@ describe('import timelines', () => { beforeEach(() => { jest.doMock('../saved_object', () => { return { - Timeline: jest.fn().mockImplementation(() => { - return { - getTimeline: mockGetTimeline.mockReturnValue(null), - persistTimeline: mockPersistTimeline.mockReturnValue({ - timeline: { savedObjectId: '79deb4c0-6bc1-11ea-9999-f5341fb7a189' }, - }), - }; + getTimeline: mockGetTimeline.mockReturnValue(null), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: { savedObjectId: '79deb4c0-6bc1-11ea-9999-f5341fb7a189' }, }), }; }); jest.doMock('../../pinned_event/saved_object', () => { return { - PinnedEvent: jest.fn().mockImplementation(() => { - return { - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline.mockReturnValue( - new Error('Test error') - ), - }; - }), + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline.mockReturnValue( + new Error('Test error') + ), }; }); jest.doMock('../../note/saved_object', () => { return { - Note: jest.fn().mockImplementation(() => { - return { - persistNote: mockPersistNote, - }; - }), + persistNote: mockPersistNote, }; }); }); @@ -328,11 +290,14 @@ describe('import timelines', () => { const importTimelinesRoute = jest.requireActual('./import_timelines_route') .importTimelinesRoute; - importTimelinesRoute(server.router, config, securitySetup); + importTimelinesRoute(server.router, createMockConfig(), securitySetup); const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'Invalid value "undefined" supplied to "file",Invalid value "undefined" supplied to "file"' + [ + 'Invalid value "undefined" supplied to "file"', + 'Invalid value "undefined" supplied to "file"', + ].join(',') ); }); }); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts index 9d148abf82cdd..bff89bdf9b5b2 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts @@ -5,9 +5,19 @@ */ import { extname } from 'path'; -import { chunk, omit, set } from 'lodash/fp'; +import { chunk, omit } from 'lodash/fp'; + +import { createPromiseFromStreams } from '../../../../../../../src/legacy/utils'; +import { IRouter } from '../../../../../../../src/core/server'; import { TIMELINE_IMPORT_URL } from '../../../../common/constants'; + +import { SetupPlugins } from '../../../plugin'; +import { ConfigType } from '../../../config'; +import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; + +import { importRulesSchema } from '../../detection_engine/routes/schemas/response/import_rules_schema'; +import { validate } from '../../detection_engine/routes/rules/validate'; import { buildSiemResponse, createBulkErrorObject, @@ -16,32 +26,22 @@ import { } from '../../detection_engine/routes/utils'; import { createTimelinesStreamFromNdJson } from '../create_timelines_stream_from_ndjson'; -import { createPromiseFromStreams } from '../../../../../../../src/legacy/utils'; +import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema'; +import { buildFrameworkRequest } from './utils/common'; import { - createTimelines, getTupleDuplicateErrorsAndUniqueTimeline, isBulkError, isImportRegular, ImportTimelineResponse, ImportTimelinesSchema, PromiseFromStreams, + timelineSavedObjectOmittedFields, } from './utils/import_timelines'; +import { createTimelines, getTimeline } from './utils/create_timelines'; -import { IRouter } from '../../../../../../../src/core/server'; -import { SetupPlugins } from '../../../plugin'; -import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema'; -import { importRulesSchema } from '../../detection_engine/routes/schemas/response/import_rules_schema'; -import { ConfigType } from '../../../config'; - -import { Timeline } from '../saved_object'; -import { validate } from '../../detection_engine/routes/rules/validate'; -import { FrameworkRequest } from '../../framework'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; const CHUNK_PARSED_OBJECT_SIZE = 10; -const timelineLib = new Timeline(); - export const importTimelinesRoute = ( router: IRouter, config: ConfigType, @@ -95,9 +95,7 @@ export const importTimelinesRoute = ( const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects); let importTimelineResponse: ImportTimelineResponse[] = []; - const user = await security?.authc.getCurrentUser(request); - let frameworkRequest = set('context.core.savedObjects.client', savedObjectsClient, request); - frameworkRequest = set('user', user, frameworkRequest); + const frameworkRequest = await buildFrameworkRequest(context, security, request); while (chunkParseObjects.length) { const batchParseObjects = chunkParseObjects.shift() ?? []; @@ -125,32 +123,16 @@ export const importTimelinesRoute = ( eventNotes, } = parsedTimeline; const parsedTimelineObject = omit( - [ - 'globalNotes', - 'eventNotes', - 'pinnedEventIds', - 'version', - 'savedObjectId', - 'created', - 'createdBy', - 'updated', - 'updatedBy', - ], + timelineSavedObjectOmittedFields, parsedTimeline ); + let newTimeline = null; try { - let timeline = null; - try { - timeline = await timelineLib.getTimeline( - (frameworkRequest as unknown) as FrameworkRequest, - savedObjectId - ); - // eslint-disable-next-line no-empty - } catch (e) {} + const timeline = await getTimeline(frameworkRequest, savedObjectId); if (timeline == null) { - const newSavedObjectId = await createTimelines( - (frameworkRequest as unknown) as FrameworkRequest, + newTimeline = await createTimelines( + frameworkRequest, parsedTimelineObject, null, // timelineSavedObjectId null, // timelineVersion @@ -159,7 +141,10 @@ export const importTimelinesRoute = ( [] // existing note ids ); - resolve({ timeline_id: newSavedObjectId, status_code: 200 }); + resolve({ + timeline_id: newTimeline.timeline.savedObjectId, + status_code: 200, + }); } else { resolve( createBulkErrorObject({ diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/create_timelines_schema.ts b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/create_timelines_schema.ts new file mode 100644 index 0000000000000..241d266a14c78 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/create_timelines_schema.ts @@ -0,0 +1,19 @@ +/* + * 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 rt from 'io-ts'; + +import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline'; +import { unionWithNullType } from '../../../../../common/utility_types'; + +export const createTimelineSchema = rt.intersection([ + rt.type({ + timeline: SavedTimelineRuntimeType, + }), + rt.partial({ + timelineId: unionWithNullType(rt.string), + version: unionWithNullType(rt.string), + }), +]); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts index 056fdaf0d2515..3b340b1c15359 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts @@ -7,14 +7,17 @@ import * as rt from 'io-ts'; import { Readable } from 'stream'; import { either } from 'fp-ts/lib/Either'; + +import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline'; + import { eventNotes, globalNotes, pinnedEventIds } from './schemas'; -import { SavedTimelineRuntimeType } from '../../types'; +import { unionWithNullType } from '../../../../../common/utility_types'; export const ImportTimelinesSchemaRt = rt.intersection([ SavedTimelineRuntimeType, rt.type({ - savedObjectId: rt.string, - version: rt.string, + savedObjectId: unionWithNullType(rt.string), + version: unionWithNullType(rt.string), }), rt.type({ globalNotes, diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts index 71627363ef0f8..1fd3a3554dc15 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import * as runtimeTypes from 'io-ts'; -import { unionWithNullType } from '../../../framework'; -import { SavedNoteRuntimeType } from '../../../note/types'; +import { unionWithNullType } from '../../../../../common/utility_types'; +import { SavedNoteRuntimeType } from '../../../../../common/types/timeline/note'; -export const eventNotes = runtimeTypes.array(unionWithNullType(SavedNoteRuntimeType)); -export const globalNotes = runtimeTypes.array(unionWithNullType(SavedNoteRuntimeType)); -export const pinnedEventIds = runtimeTypes.array(unionWithNullType(runtimeTypes.string)); +export const eventNotes = unionWithNullType(runtimeTypes.array(SavedNoteRuntimeType)); +export const globalNotes = unionWithNullType(runtimeTypes.array(SavedNoteRuntimeType)); +export const pinnedEventIds = unionWithNullType(runtimeTypes.array(runtimeTypes.string)); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/update_timelines_schema.ts b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/update_timelines_schema.ts new file mode 100644 index 0000000000000..43f4208947aa5 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/update_timelines_schema.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 * as rt from 'io-ts'; + +import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline'; +import { unionWithNullType } from '../../../../../common/utility_types'; + +export const updateTimelineSchema = rt.type({ + timeline: SavedTimelineRuntimeType, + timelineId: unionWithNullType(rt.string), + version: unionWithNullType(rt.string), +}); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts new file mode 100644 index 0000000000000..9c47488d47159 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts @@ -0,0 +1,288 @@ +/* + * 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 { SecurityPluginSetup } from '../../../../../../plugins/security/server'; + +import { + serverMock, + requestContextMock, + createMockConfig, +} from '../../detection_engine/routes/__mocks__'; + +import { + getUpdateTimelinesRequest, + inputTimeline, + updateTimelineWithTimelineId, + updateTemplateTimelineWithTimelineId, +} from './__mocks__/request_responses'; +import { + mockGetCurrentUser, + mockGetTimelineValue, + mockGetTemplateTimelineValue, +} from './__mocks__/import_timelines'; +import { + UPDATE_TIMELINE_ERROR_MESSAGE, + UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, +} from './utils/update_timelines'; + +describe('update timelines', () => { + let server: ReturnType; + let securitySetup: SecurityPluginSetup; + let { context } = requestContextMock.createTools(); + let mockGetTimeline: jest.Mock; + let mockGetTemplateTimeline: jest.Mock; + let mockPersistTimeline: jest.Mock; + let mockPersistPinnedEventOnTimeline: jest.Mock; + let mockPersistNote: jest.Mock; + + beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + + server = serverMock.create(); + context = requestContextMock.createTools().context; + + securitySetup = ({ + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown) as SecurityPluginSetup; + + mockGetTimeline = jest.fn(); + mockGetTemplateTimeline = jest.fn(); + mockPersistTimeline = jest.fn(); + mockPersistPinnedEventOnTimeline = jest.fn(); + mockPersistNote = jest.fn(); + }); + + describe('Manipulate timeline', () => { + describe('Update an existing timeline', () => { + beforeEach(async () => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: updateTimelineWithTimelineId.timeline, + }), + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const updateTimelinesRoute = jest.requireActual('./update_timelines_route') + .updateTimelinesRoute; + updateTimelinesRoute(server.router, createMockConfig(), securitySetup); + + const mockRequest = getUpdateTimelinesRequest(updateTimelineWithTimelineId); + await server.inject(mockRequest, context); + }); + + test('should Check a if given timeline id exist', async () => { + expect(mockGetTimeline.mock.calls[0][1]).toEqual(updateTimelineWithTimelineId.timelineId); + }); + + test('should Update existing timeline savedObject with timelineId', async () => { + expect(mockPersistTimeline.mock.calls[0][1]).toEqual( + updateTimelineWithTimelineId.timelineId + ); + }); + + test('should Update existing timeline savedObject with timeline version', async () => { + expect(mockPersistTimeline.mock.calls[0][2]).toEqual(updateTimelineWithTimelineId.version); + }); + + test('should Update existing timeline savedObject witn given timeline', async () => { + expect(mockPersistTimeline.mock.calls[0][3]).toEqual(inputTimeline); + }); + + test('should NOT Update new pinned events', async () => { + expect(mockPersistPinnedEventOnTimeline).not.toBeCalled(); + }); + + test('should NOT Update notes', async () => { + expect(mockPersistNote).not.toBeCalled(); + }); + + test('returns 200 when create timeline successfully', async () => { + const response = await server.inject( + getUpdateTimelinesRequest(updateTimelineWithTimelineId), + context + ); + expect(response.status).toEqual(200); + }); + }); + + describe("Update a timeline that doesn't exist", () => { + beforeEach(() => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(null), + getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null), + persistTimeline: mockPersistTimeline, + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const updateTimelinesRoute = jest.requireActual('./update_timelines_route') + .updateTimelinesRoute; + updateTimelinesRoute(server.router, createMockConfig(), securitySetup); + }); + + test('returns error message', async () => { + const response = await server.inject( + getUpdateTimelinesRequest(updateTimelineWithTimelineId), + context + ); + expect(response.body).toEqual({ + message: UPDATE_TIMELINE_ERROR_MESSAGE, + status_code: 405, + }); + }); + }); + }); + + describe('Manipulate template timeline', () => { + describe('Update an existing template timeline', () => { + beforeEach(async () => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), + getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ + timeline: [mockGetTemplateTimelineValue], + }), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: updateTimelineWithTimelineId.timeline, + }), + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const updateTimelinesRoute = jest.requireActual('./update_timelines_route') + .updateTimelinesRoute; + updateTimelinesRoute(server.router, createMockConfig(), securitySetup); + + const mockRequest = getUpdateTimelinesRequest(updateTemplateTimelineWithTimelineId); + await server.inject(mockRequest, context); + }); + + test('should Check if given timeline id exist', async () => { + expect(mockGetTimeline.mock.calls[0][1]).toEqual( + updateTemplateTimelineWithTimelineId.timelineId + ); + }); + + test('should Update existing template timeline with template timelineId', async () => { + expect(mockGetTemplateTimeline.mock.calls[0][1]).toEqual( + updateTemplateTimelineWithTimelineId.timelineId + ); + }); + + test('should Update existing template timeline with timeline version', async () => { + expect(mockPersistTimeline.mock.calls[0][2]).toEqual( + updateTemplateTimelineWithTimelineId.version + ); + }); + + test('should Update existing template timeline witn given timeline', async () => { + expect(mockPersistTimeline.mock.calls[0][3]).toEqual( + updateTemplateTimelineWithTimelineId.timeline + ); + }); + + test('should NOT Update new pinned events', async () => { + expect(mockPersistPinnedEventOnTimeline).not.toBeCalled(); + }); + + test('should NOT Update notes', async () => { + expect(mockPersistNote).not.toBeCalled(); + }); + + test('returns 200 when create template timeline successfully', async () => { + const response = await server.inject( + getUpdateTimelinesRequest(updateTemplateTimelineWithTimelineId), + context + ); + expect(response.status).toEqual(200); + }); + }); + + describe("Update a template timeline that doesn't exist", () => { + beforeEach(() => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(null), + getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ + timeline: [], + }), + persistTimeline: mockPersistTimeline, + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const updateTimelinesRoute = jest.requireActual('./update_timelines_route') + .updateTimelinesRoute; + updateTimelinesRoute(server.router, createMockConfig(), securitySetup); + }); + + test('returns error message', async () => { + const response = await server.inject( + getUpdateTimelinesRequest(updateTemplateTimelineWithTimelineId), + context + ); + expect(response.body).toEqual({ + message: UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + status_code: 405, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.ts new file mode 100644 index 0000000000000..a0f3d11a1533d --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.ts @@ -0,0 +1,87 @@ +/* + * 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 { IRouter } from '../../../../../../../src/core/server'; + +import { TIMELINE_URL } from '../../../../common/constants'; +import { TimelineType } from '../../../../common/types/timeline'; + +import { SetupPlugins } from '../../../plugin'; +import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { ConfigType } from '../../..'; + +import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; +import { FrameworkRequest } from '../../framework'; + +import { updateTimelineSchema } from './schemas/update_timelines_schema'; +import { buildFrameworkRequest } from './utils/common'; +import { createTimelines, getTimeline, getTemplateTimeline } from './utils/create_timelines'; +import { checkIsFailureCases } from './utils/update_timelines'; + +export const updateTimelinesRoute = ( + router: IRouter, + config: ConfigType, + security: SetupPlugins['security'] +) => { + router.patch( + { + path: TIMELINE_URL, + validate: { + body: buildRouteValidation(updateTimelineSchema), + }, + options: { + tags: ['access:siem'], + }, + }, + // eslint-disable-next-line complexity + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const frameworkRequest = await buildFrameworkRequest(context, security, request); + const { timelineId, timeline, version } = request.body; + const { templateTimelineId, templateTimelineVersion, timelineType } = timeline; + const isHandlingTemplateTimeline = timelineType === TimelineType.template; + const existTimeline = + timelineId != null ? await getTimeline(frameworkRequest, timelineId) : null; + + const existTemplateTimeline = + templateTimelineId != null + ? await getTemplateTimeline(frameworkRequest, templateTimelineId) + : null; + const errorObj = checkIsFailureCases( + isHandlingTemplateTimeline, + version, + templateTimelineVersion ?? null, + existTimeline, + existTemplateTimeline + ); + if (errorObj != null) { + return siemResponse.error(errorObj); + } + const updatedTimeline = await createTimelines( + (frameworkRequest as unknown) as FrameworkRequest, + timeline, + timelineId, + version + ); + return response.ok({ + body: { + data: { + persistTimeline: updatedTimeline, + }, + }, + }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts new file mode 100644 index 0000000000000..1036a74b74a03 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.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 { set } from 'lodash/fp'; + +import { SetupPlugins } from '../../../../plugin'; +import { KibanaRequest } from '../../../../../../../../src/core/server'; +import { RequestHandlerContext } from '../../../../../../../../target/types/core/server'; +import { FrameworkRequest } from '../../../framework'; + +export const buildFrameworkRequest = async ( + context: RequestHandlerContext, + security: SetupPlugins['security'], + request: KibanaRequest +): Promise => { + const savedObjectsClient = context.core.savedObjects.client; + const user = await security?.authc.getCurrentUser(request); + + return set( + 'user', + user, + set( + 'context.core.savedObjects.client', + savedObjectsClient, + request + ) + ); +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/create_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/create_timelines.ts new file mode 100644 index 0000000000000..2c67a514cdf97 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/create_timelines.ts @@ -0,0 +1,150 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; + +import * as timelineLib from '../../saved_object'; +import * as pinnedEventLib from '../../../pinned_event/saved_object'; +import * as noteLib from '../../../note/saved_object'; +import { FrameworkRequest } from '../../../framework'; +import { SavedTimeline, TimelineSavedObject } from '../../../../../common/types/timeline'; +import { SavedNote } from '../../../../../common/types/timeline/note'; +import { NoteResult, ResponseTimeline } from '../../../../graphql/types'; +export const CREATE_TIMELINE_ERROR_MESSAGE = + 'UPDATE timeline with POST is not allowed, please use PATCH instead'; +export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE = + 'UPDATE template timeline with POST is not allowed, please use PATCH instead'; + +export const saveTimelines = ( + frameworkRequest: FrameworkRequest, + timeline: SavedTimeline, + timelineSavedObjectId?: string | null, + timelineVersion?: string | null +): Promise => { + return timelineLib.persistTimeline( + frameworkRequest, + timelineSavedObjectId ?? null, + timelineVersion ?? null, + timeline + ); +}; + +export const savePinnedEvents = ( + frameworkRequest: FrameworkRequest, + timelineSavedObjectId: string, + pinnedEventIds: string[] +) => + Promise.all( + pinnedEventIds.map(eventId => + pinnedEventLib.persistPinnedEventOnTimeline( + frameworkRequest, + null, // pinnedEventSavedObjectId + eventId, + timelineSavedObjectId + ) + ) + ); + +export const saveNotes = ( + frameworkRequest: FrameworkRequest, + timelineSavedObjectId: string, + timelineVersion?: string | null, + existingNoteIds?: string[], + newNotes?: NoteResult[] +) => { + return Promise.all( + newNotes?.map(note => { + const newNote: SavedNote = { + eventId: note.eventId, + note: note.note, + timelineId: timelineSavedObjectId, + }; + + return noteLib.persistNote( + frameworkRequest, + existingNoteIds?.find(nId => nId === note.noteId) ?? null, + timelineVersion ?? null, + newNote + ); + }) ?? [] + ); +}; + +export const createTimelines = async ( + frameworkRequest: FrameworkRequest, + timeline: SavedTimeline, + timelineSavedObjectId?: string | null, + timelineVersion?: string | null, + pinnedEventIds?: string[] | null, + notes?: NoteResult[], + existingNoteIds?: string[] +): Promise => { + const responseTimeline = await saveTimelines( + frameworkRequest, + timeline, + timelineSavedObjectId, + timelineVersion + ); + const newTimelineSavedObjectId = responseTimeline.timeline.savedObjectId; + const newTimelineVersion = responseTimeline.timeline.version; + + let myPromises: unknown[] = []; + if (pinnedEventIds != null && !isEmpty(pinnedEventIds)) { + myPromises = [ + ...myPromises, + savePinnedEvents( + frameworkRequest, + timelineSavedObjectId ?? newTimelineSavedObjectId, + pinnedEventIds + ), + ]; + } + if (!isEmpty(notes)) { + myPromises = [ + ...myPromises, + saveNotes( + frameworkRequest, + timelineSavedObjectId ?? newTimelineSavedObjectId, + newTimelineVersion, + existingNoteIds, + notes + ), + ]; + } + + if (myPromises.length > 0) { + await Promise.all(myPromises); + } + + return responseTimeline; +}; + +export const getTimeline = async ( + frameworkRequest: FrameworkRequest, + savedObjectId: string +): Promise => { + let timeline = null; + try { + timeline = await timelineLib.getTimeline(frameworkRequest, savedObjectId); + // eslint-disable-next-line no-empty + } catch (e) {} + return timeline; +}; + +export const getTemplateTimeline = async ( + frameworkRequest: FrameworkRequest, + templateTimelineId: string +): Promise => { + let templateTimeline = null; + try { + templateTimeline = await timelineLib.getTimelineByTemplateTimelineId( + frameworkRequest, + templateTimelineId + ); + } catch (e) { + return null; + } + return templateTimeline.timeline[0]; +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts index 677891fa16c02..ea9a5fab66805 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts @@ -4,13 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NoteSavedObject } from '../../../note/types'; -import { PinnedEventSavedObject } from '../../../pinned_event/types'; -import { convertSavedObjectToSavedTimeline } from '../../convert_saved_object_to_savedtimeline'; - -import { convertSavedObjectToSavedPinnedEvent } from '../../../pinned_event/saved_object'; -import { convertSavedObjectToSavedNote } from '../../../note/saved_object'; - import { SavedObjectsClient, SavedObjectsFindOptions, @@ -22,11 +15,20 @@ import { ExportTimelineSavedObjectsClient, ExportedNotes, TimelineSavedObject, -} from '../../types'; + ExportTimelineNotFoundError, +} from '../../../../../common/types/timeline'; +import { NoteSavedObject } from '../../../../../common/types/timeline/note'; +import { PinnedEventSavedObject } from '../../../../../common/types/timeline/pinned_event'; + import { transformDataToNdjson } from '../../../../utils/read_stream/create_stream_from_ndjson'; + +import { convertSavedObjectToSavedPinnedEvent } from '../../../pinned_event/saved_object'; +import { convertSavedObjectToSavedNote } from '../../../note/saved_object'; import { pinnedEventSavedObjectType } from '../../../pinned_event/saved_object_mappings'; import { noteSavedObjectType } from '../../../note/saved_object_mappings'; + import { timelineSavedObjectType } from '../../saved_object_mappings'; +import { convertSavedObjectToSavedTimeline } from '../../convert_saved_object_to_savedtimeline'; export type TimelineSavedObjectsClient = Pick< SavedObjectsClient, @@ -126,12 +128,23 @@ const getTimelines = async ( ) ); - const timelineObjects: TimelineSavedObject[] | undefined = - savedObjects != null - ? savedObjects.saved_objects.map((savedObject: unknown) => { - return convertSavedObjectToSavedTimeline(savedObject); - }) - : []; + const timelineObjects: { + timelines: TimelineSavedObject[]; + errors: ExportTimelineNotFoundError[]; + } = savedObjects.saved_objects.reduce( + (acc, savedObject) => { + return savedObject.error == null + ? { + errors: acc.errors, + timelines: [...acc.timelines, convertSavedObjectToSavedTimeline(savedObject)], + } + : { errors: [...acc.errors, savedObject.error], timelines: acc.timelines }; + }, + { + timelines: [] as TimelineSavedObject[], + errors: [] as ExportTimelineNotFoundError[], + } + ); return timelineObjects; }; @@ -139,12 +152,8 @@ const getTimelines = async ( const getTimelinesFromObjects = async ( savedObjectsClient: ExportTimelineSavedObjectsClient, ids: string[] -): Promise => { - const timelines: TimelineSavedObject[] = await getTimelines(savedObjectsClient, ids); - // To Do for feature freeze - // if (timelines.length !== request.body.ids.length) { - // //figure out which is missing to tell user - // } +): Promise> => { + const { timelines, errors } = await getTimelines(savedObjectsClient, ids); const [notes, pinnedEventIds] = await Promise.all([ Promise.all(ids.map(timelineId => getNotesByTimelineId(savedObjectsClient, timelineId))), @@ -178,7 +187,7 @@ const getTimelinesFromObjects = async ( return acc; }, []); - return myResponse ?? []; + return [...myResponse, ...errors] ?? []; }; export const getExportTimelineByObjectIds = async ({ diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts index f69a715f9b2c9..9e120cdc023dc 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts @@ -7,20 +7,10 @@ import uuid from 'uuid'; import { has } from 'lodash/fp'; import { createBulkErrorObject, BulkError } from '../../../detection_engine/routes/utils'; -import { PinnedEvent } from '../../../pinned_event/saved_object'; -import { Note } from '../../../note/saved_object'; - -import { Timeline } from '../../saved_object'; -import { SavedTimeline } from '../../types'; -import { FrameworkRequest } from '../../../framework'; -import { SavedNote } from '../../../note/types'; +import { SavedTimeline } from '../../../../../common/types/timeline'; import { NoteResult } from '../../../../graphql/types'; import { HapiReadableStream } from '../../../detection_engine/rules/types'; -const pinnedEventLib = new PinnedEvent(); -const timelineLib = new Timeline(); -const noteLib = new Note(); - export interface ImportTimelinesSchema { success: boolean; success_count: number; @@ -84,100 +74,6 @@ export const getTupleDuplicateErrorsAndUniqueTimeline = ( return [Array.from(errors.values()), Array.from(timelinesAcc.values())]; }; -export const saveTimelines = async ( - frameworkRequest: FrameworkRequest, - timeline: SavedTimeline, - timelineSavedObjectId?: string | null, - timelineVersion?: string | null -) => { - const newTimelineRes = await timelineLib.persistTimeline( - frameworkRequest, - timelineSavedObjectId ?? null, - timelineVersion ?? null, - timeline - ); - - return { - newTimelineSavedObjectId: newTimelineRes?.timeline?.savedObjectId ?? null, - newTimelineVersion: newTimelineRes?.timeline?.version ?? null, - }; -}; - -export const savePinnedEvents = ( - frameworkRequest: FrameworkRequest, - timelineSavedObjectId: string, - pinnedEventIds?: string[] | null -) => { - return ( - pinnedEventIds?.map(eventId => { - return pinnedEventLib.persistPinnedEventOnTimeline( - frameworkRequest, - null, // pinnedEventSavedObjectId - eventId, - timelineSavedObjectId - ); - }) ?? [] - ); -}; - -export const saveNotes = ( - frameworkRequest: FrameworkRequest, - timelineSavedObjectId: string, - timelineVersion?: string | null, - existingNoteIds?: string[], - newNotes?: NoteResult[] -) => { - return Promise.all( - newNotes?.map(note => { - const newNote: SavedNote = { - eventId: note.eventId, - note: note.note, - timelineId: timelineSavedObjectId, - }; - - return noteLib.persistNote( - frameworkRequest, - existingNoteIds?.find(nId => nId === note.noteId) ?? null, - timelineVersion ?? null, - newNote - ); - }) ?? [] - ); -}; - -export const createTimelines = async ( - frameworkRequest: FrameworkRequest, - timeline: SavedTimeline, - timelineSavedObjectId?: string | null, - timelineVersion?: string | null, - pinnedEventIds?: string[] | null, - notes?: NoteResult[], - existingNoteIds?: string[] -) => { - const { newTimelineSavedObjectId, newTimelineVersion } = await saveTimelines( - frameworkRequest, - timeline, - timelineSavedObjectId, - timelineVersion - ); - await Promise.all([ - savePinnedEvents( - frameworkRequest, - timelineSavedObjectId ?? newTimelineSavedObjectId, - pinnedEventIds - ), - saveNotes( - frameworkRequest, - timelineSavedObjectId ?? newTimelineSavedObjectId, - newTimelineVersion, - existingNoteIds, - notes - ), - ]); - - return newTimelineSavedObjectId; -}; - export const isImportRegular = ( importTimelineResponse: ImportTimelineResponse ): importTimelineResponse is ImportRegular => { @@ -189,3 +85,15 @@ export const isBulkError = ( ): importRuleResponse is BulkError => { return has('error', importRuleResponse); }; + +export const timelineSavedObjectOmittedFields = [ + 'globalNotes', + 'eventNotes', + 'pinnedEventIds', + 'version', + 'savedObjectId', + 'created', + 'createdBy', + 'updated', + 'updatedBy', +]; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts new file mode 100644 index 0000000000000..6a25d8def9116 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.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 { TimelineSavedObject } from '../../../../../common/types/timeline'; + +export const UPDATE_TIMELINE_ERROR_MESSAGE = + 'CREATE timeline with PATCH is not allowed, please use POST instead'; +export const UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE = + 'CREATE template timeline with PATCH is not allowed, please use POST instead'; +export const NO_MATCH_VERSION_ERROR_MESSAGE = + 'TimelineVersion conflict: The given version doesn not match with existing timeline'; +export const NO_MATCH_ID_ERROR_MESSAGE = + "Timeline id doesn't match with existing template timeline"; +export const OLDER_VERSION_ERROR_MESSAGE = + 'Template timelineVersion conflict: The given version is older then existing version'; + +export const checkIsFailureCases = ( + isHandlingTemplateTimeline: boolean, + version: string | null, + templateTimelineVersion: number | null, + existTimeline: TimelineSavedObject | null, + existTemplateTimeline: TimelineSavedObject | null +) => { + if (!isHandlingTemplateTimeline && existTimeline == null) { + return { + body: UPDATE_TIMELINE_ERROR_MESSAGE, + statusCode: 405, + }; + } else if (isHandlingTemplateTimeline && existTemplateTimeline == null) { + // Throw error to create template timeline in patch + return { + body: UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + statusCode: 405, + }; + } else if ( + isHandlingTemplateTimeline && + existTimeline != null && + existTemplateTimeline != null && + existTimeline.savedObjectId !== existTemplateTimeline.savedObjectId + ) { + // Throw error you can not have a no matching between your timeline and your template timeline during an update + return { + body: NO_MATCH_ID_ERROR_MESSAGE, + statusCode: 409, + }; + } else if (!isHandlingTemplateTimeline && existTimeline?.version !== version) { + // throw error 409 conflict timeline + return { + body: NO_MATCH_VERSION_ERROR_MESSAGE, + statusCode: 409, + }; + } else if ( + isHandlingTemplateTimeline && + existTemplateTimeline != null && + existTemplateTimeline.templateTimelineVersion == null && + existTemplateTimeline.version !== version + ) { + // throw error 409 conflict timeline + return { + body: NO_MATCH_VERSION_ERROR_MESSAGE, + statusCode: 409, + }; + } else if ( + isHandlingTemplateTimeline && + templateTimelineVersion != null && + existTemplateTimeline != null && + existTemplateTimeline.templateTimelineVersion != null && + existTemplateTimeline.templateTimelineVersion >= templateTimelineVersion + ) { + // Throw error you can not update a template timeline version with an old version + return { + body: OLDER_VERSION_ERROR_MESSAGE, + statusCode: 409, + }; + } else { + return null; + } +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts index e8cd27947589f..d2df7589f3c4a 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts @@ -8,260 +8,308 @@ import { getOr } from 'lodash/fp'; import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; import { UNAUTHENTICATED_USER } from '../../../common/constants'; +import { NoteSavedObject } from '../../../common/types/timeline/note'; +import { PinnedEventSavedObject } from '../../../common/types/timeline/pinned_event'; +import { SavedTimeline, TimelineSavedObject, TimelineType } from '../../../common/types/timeline'; import { ResponseTimeline, PageInfoTimeline, SortTimeline, ResponseFavoriteTimeline, TimelineResult, + Maybe, } from '../../graphql/types'; import { FrameworkRequest } from '../framework'; -import { Note } from '../note/saved_object'; -import { NoteSavedObject } from '../note/types'; -import { PinnedEventSavedObject } from '../pinned_event/types'; -import { PinnedEvent } from '../pinned_event/saved_object'; +import * as note from '../note/saved_object'; +import * as pinnedEvent from '../pinned_event/saved_object'; import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; import { pickSavedTimeline } from './pick_saved_timeline'; import { timelineSavedObjectType } from './saved_object_mappings'; -import { SavedTimeline, TimelineSavedObject } from './types'; interface ResponseTimelines { timeline: TimelineSavedObject[]; totalCount: number; } -export class Timeline { - private readonly note = new Note(); - private readonly pinnedEvent = new PinnedEvent(); +export interface ResponseTemplateTimeline { + code?: Maybe; - public async getTimeline( - request: FrameworkRequest, - timelineId: string - ): Promise { - return this.getSavedTimeline(request, timelineId); - } + message?: Maybe; + + templateTimeline: TimelineResult; +} - public async getAllTimeline( +export interface Timeline { + getTimeline: (request: FrameworkRequest, timelineId: string) => Promise; + + getAllTimeline: ( request: FrameworkRequest, onlyUserFavorite: boolean | null, pageInfo: PageInfoTimeline | null, search: string | null, sort: SortTimeline | null - ): Promise { - const options: SavedObjectsFindOptions = { - type: timelineSavedObjectType, - perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex : undefined, - search: search != null ? search : undefined, - searchFields: onlyUserFavorite - ? ['title', 'description', 'favorite.keySearch'] - : ['title', 'description'], - sortField: sort != null ? sort.sortField : undefined, - sortOrder: sort != null ? sort.sortOrder : undefined, - }; - - return this.getAllSavedTimeline(request, options); - } + ) => Promise; - public async persistFavorite( + persistFavorite: ( request: FrameworkRequest, timelineId: string | null - ): Promise { - const userName = request.user?.username ?? UNAUTHENTICATED_USER; - const fullName = request.user?.full_name ?? ''; - try { - let timeline: SavedTimeline = {}; - if (timelineId != null) { - const { - eventIdToNoteIds, - notes, - noteIds, - pinnedEventIds, - pinnedEventsSaveObject, - savedObjectId, - version, - ...savedTimeline - } = await this.getBasicSavedTimeline(request, timelineId); - timelineId = savedObjectId; // eslint-disable-line no-param-reassign - timeline = savedTimeline; - } + ) => Promise; - const userFavoriteTimeline = { - keySearch: userName != null ? convertStringToBase64(userName) : null, - favoriteDate: new Date().valueOf(), - fullName, - userName, - }; - if (timeline.favorite != null) { - const alreadyExistsTimelineFavoriteByUser = timeline.favorite.findIndex( - user => user.userName === userName - ); - - timeline.favorite = - alreadyExistsTimelineFavoriteByUser > -1 - ? [ - ...timeline.favorite.slice(0, alreadyExistsTimelineFavoriteByUser), - ...timeline.favorite.slice(alreadyExistsTimelineFavoriteByUser + 1), - ] - : [...timeline.favorite, userFavoriteTimeline]; - } else if (timeline.favorite == null) { - timeline.favorite = [userFavoriteTimeline]; - } + persistTimeline: ( + request: FrameworkRequest, + timelineId: string | null, + version: string | null, + timeline: SavedTimeline, + timelineType?: TimelineType | null + ) => Promise; - const persistResponse = await this.persistTimeline(request, timelineId, null, timeline); + deleteTimeline: (request: FrameworkRequest, timelineIds: string[]) => Promise; + convertStringToBase64: (text: string) => string; + timelineWithReduxProperties: ( + notes: NoteSavedObject[], + pinnedEvents: PinnedEventSavedObject[], + timeline: TimelineSavedObject, + userName: string + ) => TimelineSavedObject; +} + +export const getTimeline = async ( + request: FrameworkRequest, + timelineId: string +): Promise => { + return getSavedTimeline(request, timelineId); +}; + +export const getTimelineByTemplateTimelineId = async ( + request: FrameworkRequest, + templateTimelineId: string +): Promise<{ + totalCount: number; + timeline: TimelineSavedObject[]; +}> => { + const options: SavedObjectsFindOptions = { + type: timelineSavedObjectType, + filter: `siem-ui-timeline.attributes.templateTimelineId: ${templateTimelineId}`, + }; + return getAllSavedTimeline(request, options); +}; + +export const getAllTimeline = async ( + request: FrameworkRequest, + onlyUserFavorite: boolean | null, + pageInfo: PageInfoTimeline | null, + search: string | null, + sort: SortTimeline | null +): Promise => { + const options: SavedObjectsFindOptions = { + type: timelineSavedObjectType, + perPage: pageInfo != null ? pageInfo.pageSize : undefined, + page: pageInfo != null ? pageInfo.pageIndex : undefined, + search: search != null ? search : undefined, + searchFields: onlyUserFavorite + ? ['title', 'description', 'favorite.keySearch'] + : ['title', 'description'], + sortField: sort != null ? sort.sortField : undefined, + sortOrder: sort != null ? sort.sortOrder : undefined, + }; + return getAllSavedTimeline(request, options); +}; + +export const persistFavorite = async ( + request: FrameworkRequest, + timelineId: string | null +): Promise => { + const userName = request.user?.username ?? UNAUTHENTICATED_USER; + const fullName = request.user?.full_name ?? ''; + try { + let timeline: SavedTimeline = {}; + if (timelineId != null) { + const { + eventIdToNoteIds, + notes, + noteIds, + pinnedEventIds, + pinnedEventsSaveObject, + savedObjectId, + version, + ...savedTimeline + } = await getBasicSavedTimeline(request, timelineId); + timelineId = savedObjectId; // eslint-disable-line no-param-reassign + timeline = savedTimeline; + } + + const userFavoriteTimeline = { + keySearch: userName != null ? convertStringToBase64(userName) : null, + favoriteDate: new Date().valueOf(), + fullName, + userName, + }; + if (timeline.favorite != null) { + const alreadyExistsTimelineFavoriteByUser = timeline.favorite.findIndex( + user => user.userName === userName + ); + + timeline.favorite = + alreadyExistsTimelineFavoriteByUser > -1 + ? [ + ...timeline.favorite.slice(0, alreadyExistsTimelineFavoriteByUser), + ...timeline.favorite.slice(alreadyExistsTimelineFavoriteByUser + 1), + ] + : [...timeline.favorite, userFavoriteTimeline]; + } else if (timeline.favorite == null) { + timeline.favorite = [userFavoriteTimeline]; + } + + const persistResponse = await persistTimeline(request, timelineId, null, timeline); + return { + savedObjectId: persistResponse.timeline.savedObjectId, + version: persistResponse.timeline.version, + favorite: + persistResponse.timeline.favorite != null + ? persistResponse.timeline.favorite.filter(fav => fav.userName === userName) + : [], + }; + } catch (err) { + if (getOr(null, 'output.statusCode', err) === 403) { return { - savedObjectId: persistResponse.timeline.savedObjectId, - version: persistResponse.timeline.version, - favorite: - persistResponse.timeline.favorite != null - ? persistResponse.timeline.favorite.filter(fav => fav.userName === userName) - : [], + savedObjectId: '', + version: '', + favorite: [], + code: 403, + message: err.message, }; - } catch (err) { - if (getOr(null, 'output.statusCode', err) === 403) { - return { - savedObjectId: '', - version: '', - favorite: [], - code: 403, - message: err.message, - }; - } - throw err; } + throw err; } +}; - public async persistTimeline( - request: FrameworkRequest, - timelineId: string | null, - version: string | null, - timeline: SavedTimeline - ): Promise { - const savedObjectsClient = request.context.core.savedObjects.client; - try { - if (timelineId == null) { - // Create new timeline - const newTimeline = convertSavedObjectToSavedTimeline( - await savedObjectsClient.create( - timelineSavedObjectType, - pickSavedTimeline(timelineId, timeline, request.user) - ) - ); - return { - code: 200, - message: 'success', - timeline: newTimeline, - }; - } - // Update Timeline - await savedObjectsClient.update( - timelineSavedObjectType, - timelineId, - pickSavedTimeline(timelineId, timeline, request.user), - { - version: version || undefined, - } +export const persistTimeline = async ( + request: FrameworkRequest, + timelineId: string | null, + version: string | null, + timeline: SavedTimeline +): Promise => { + const savedObjectsClient = request.context.core.savedObjects.client; + try { + if (timelineId == null) { + // Create new timeline + const newTimeline = convertSavedObjectToSavedTimeline( + await savedObjectsClient.create( + timelineSavedObjectType, + pickSavedTimeline(timelineId, timeline, request.user) + ) ); - return { code: 200, message: 'success', - timeline: await this.getSavedTimeline(request, timelineId), + timeline: newTimeline, }; - } catch (err) { - if (timelineId != null && savedObjectsClient.errors.isConflictError(err)) { - return { - code: 409, - message: err.message, - timeline: await this.getSavedTimeline(request, timelineId), - }; - } else if (getOr(null, 'output.statusCode', err) === 403) { - const timelineToReturn: TimelineResult = { - ...timeline, - savedObjectId: '', - version: '', - }; - return { - code: 403, - message: err.message, - timeline: timelineToReturn, - }; + } + // Update Timeline + await savedObjectsClient.update( + timelineSavedObjectType, + timelineId, + pickSavedTimeline(timelineId, timeline, request.user), + { + version: version || undefined, } - throw err; + ); + + return { + code: 200, + message: 'success', + timeline: await getSavedTimeline(request, timelineId), + }; + } catch (err) { + if (timelineId != null && savedObjectsClient.errors.isConflictError(err)) { + return { + code: 409, + message: err.message, + timeline: await getSavedTimeline(request, timelineId), + }; + } else if (getOr(null, 'output.statusCode', err) === 403) { + const timelineToReturn: TimelineResult = { + ...timeline, + savedObjectId: '', + version: '', + }; + return { + code: 403, + message: err.message, + timeline: timelineToReturn, + }; } + throw err; } +}; - public async deleteTimeline(request: FrameworkRequest, timelineIds: string[]) { - const savedObjectsClient = request.context.core.savedObjects.client; - - await Promise.all( - timelineIds.map(timelineId => - Promise.all([ - savedObjectsClient.delete(timelineSavedObjectType, timelineId), - this.note.deleteNoteByTimelineId(request, timelineId), - this.pinnedEvent.deleteAllPinnedEventsOnTimeline(request, timelineId), - ]) - ) - ); - } +export const deleteTimeline = async (request: FrameworkRequest, timelineIds: string[]) => { + const savedObjectsClient = request.context.core.savedObjects.client; - private async getBasicSavedTimeline(request: FrameworkRequest, timelineId: string) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); + await Promise.all( + timelineIds.map(timelineId => + Promise.all([ + savedObjectsClient.delete(timelineSavedObjectType, timelineId), + note.deleteNoteByTimelineId(request, timelineId), + pinnedEvent.deleteAllPinnedEventsOnTimeline(request, timelineId), + ]) + ) + ); +}; - return convertSavedObjectToSavedTimeline(savedObject); - } +const getBasicSavedTimeline = async (request: FrameworkRequest, timelineId: string) => { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); + + return convertSavedObjectToSavedTimeline(savedObject); +}; - private async getSavedTimeline(request: FrameworkRequest, timelineId: string) { - const userName = request.user?.username ?? UNAUTHENTICATED_USER; +const getSavedTimeline = async (request: FrameworkRequest, timelineId: string) => { + const userName = request.user?.username ?? UNAUTHENTICATED_USER; - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); - const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); - const timelineWithNotesAndPinnedEvents = await Promise.all([ - this.note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), - this.pinnedEvent.getAllPinnedEventsByTimelineId(request, timelineSaveObject.savedObjectId), - Promise.resolve(timelineSaveObject), - ]); + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); + const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); + const timelineWithNotesAndPinnedEvents = await Promise.all([ + note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), + pinnedEvent.getAllPinnedEventsByTimelineId(request, timelineSaveObject.savedObjectId), + Promise.resolve(timelineSaveObject), + ]); - const [notes, pinnedEvents, timeline] = timelineWithNotesAndPinnedEvents; + const [notes, pinnedEvents, timeline] = timelineWithNotesAndPinnedEvents; - return timelineWithReduxProperties(notes, pinnedEvents, timeline, userName); + return timelineWithReduxProperties(notes, pinnedEvents, timeline, userName); +}; + +const getAllSavedTimeline = async (request: FrameworkRequest, options: SavedObjectsFindOptions) => { + const userName = request.user?.username ?? UNAUTHENTICATED_USER; + const savedObjectsClient = request.context.core.savedObjects.client; + if (options.searchFields != null && options.searchFields.includes('favorite.keySearch')) { + options.search = `${options.search != null ? options.search : ''} ${ + userName != null ? convertStringToBase64(userName) : null + }`; } - private async getAllSavedTimeline(request: FrameworkRequest, options: SavedObjectsFindOptions) { - const userName = request.user?.username ?? UNAUTHENTICATED_USER; - const savedObjectsClient = request.context.core.savedObjects.client; - if (options.searchFields != null && options.searchFields.includes('favorite.keySearch')) { - options.search = `${options.search != null ? options.search : ''} ${ - userName != null ? convertStringToBase64(userName) : null - }`; - } + const savedObjects = await savedObjectsClient.find(options); - const savedObjects = await savedObjectsClient.find(options); - - const timelinesWithNotesAndPinnedEvents = await Promise.all( - savedObjects.saved_objects.map(async savedObject => { - const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); - return Promise.all([ - this.note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), - this.pinnedEvent.getAllPinnedEventsByTimelineId( - request, - timelineSaveObject.savedObjectId - ), - Promise.resolve(timelineSaveObject), - ]); - }) - ); + const timelinesWithNotesAndPinnedEvents = await Promise.all( + savedObjects.saved_objects.map(async savedObject => { + const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); + return Promise.all([ + note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), + pinnedEvent.getAllPinnedEventsByTimelineId(request, timelineSaveObject.savedObjectId), + Promise.resolve(timelineSaveObject), + ]); + }) + ); - return { - totalCount: savedObjects.total, - timeline: timelinesWithNotesAndPinnedEvents.map(([notes, pinnedEvents, timeline]) => - timelineWithReduxProperties(notes, pinnedEvents, timeline, userName) - ), - }; - } -} + return { + totalCount: savedObjects.total, + timeline: timelinesWithNotesAndPinnedEvents.map(([notes, pinnedEvents, timeline]) => + timelineWithReduxProperties(notes, pinnedEvents, timeline, userName) + ), + }; +}; export const convertStringToBase64 = (text: string): string => Buffer.from(text).toString('base64'); @@ -283,11 +331,9 @@ export const timelineWithReduxProperties = ( timeline.favorite != null && userName != null ? timeline.favorite.filter(fav => fav.userName === userName) : [], - eventIdToNoteIds: notes.filter(note => note.eventId != null), - noteIds: notes - .filter(note => note.eventId == null && note.noteId != null) - .map(note => note.noteId), + eventIdToNoteIds: notes.filter(n => n.eventId != null), + noteIds: notes.filter(n => n.eventId == null && n.noteId != null).map(n => n.noteId), notes, - pinnedEventIds: pinnedEvents.map(pinnedEvent => pinnedEvent.eventId), + pinnedEventIds: pinnedEvents.map(e => e.eventId), pinnedEventsSaveObject: pinnedEvents, }); diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts index b956e0f98fcb6..1cab24d0879ff 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts @@ -231,6 +231,15 @@ export const timelineSavedObjectMappings = { title: { type: 'text', }, + templateTimelineId: { + type: 'text', + }, + templateTimelineVersion: { + type: 'integer', + }, + timelineType: { + type: 'keyword', + }, dateRange: { properties: { start: { diff --git a/x-pack/plugins/siem/server/routes/index.ts b/x-pack/plugins/siem/server/routes/index.ts index 1c03823e85fd7..ffad86a09cee7 100644 --- a/x-pack/plugins/siem/server/routes/index.ts +++ b/x-pack/plugins/siem/server/routes/index.ts @@ -30,6 +30,8 @@ import { findRulesStatusesRoute } from '../lib/detection_engine/routes/rules/fin import { getPrepackagedRulesStatusRoute } from '../lib/detection_engine/routes/rules/get_prepackaged_rules_status_route'; import { importTimelinesRoute } from '../lib/timeline/routes/import_timelines_route'; import { exportTimelinesRoute } from '../lib/timeline/routes/export_timelines_route'; +import { createTimelinesRoute } from '../lib/timeline/routes/create_timelines_route'; +import { updateTimelinesRoute } from '../lib/timeline/routes/update_timelines_route'; import { SetupPlugins } from '../plugin'; import { ConfigType } from '../config'; @@ -55,6 +57,8 @@ export const initRoutes = ( patchRulesBulkRoute(router); deleteRulesBulkRoute(router); + createTimelinesRoute(router, config, security); + updateTimelinesRoute(router, config, security); importRulesRoute(router, config); exportRulesRoute(router, config); diff --git a/x-pack/plugins/siem/server/utils/typed_resolvers.ts b/x-pack/plugins/siem/server/utils/typed_resolvers.ts index da38e8a1e1bf2..4f19bd54b01f0 100644 --- a/x-pack/plugins/siem/server/utils/typed_resolvers.ts +++ b/x-pack/plugins/siem/server/utils/typed_resolvers.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as runtimeTypes from 'io-ts'; import { GraphQLResolveInfo } from 'graphql'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -106,6 +105,3 @@ export type ChildResolverOf = ResolverWithParent< Resolver_, ResultOf >; - -export const unionWithNullType = (type: T) => - runtimeTypes.union([type, runtimeTypes.null]); diff --git a/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts b/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts index b063404f68e4f..65a810ff94a1f 100644 --- a/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts +++ b/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts @@ -6,7 +6,7 @@ import { SavedObjectMigrationFn } from 'src/core/server'; -export const migrateToKibana660: SavedObjectMigrationFn = doc => { +export const migrateToKibana660: SavedObjectMigrationFn = doc => { if (!doc.attributes.hasOwnProperty('disabledFeatures')) { doc.attributes.disabledFeatures = []; } diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index a70fbdb18c30b..0f6e3fc31d96d 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -12,11 +12,10 @@ import { TaskManager } from './task_manager'; import { createTaskManager } from './create_task_manager'; import { TaskManagerConfig } from './config'; import { Middleware } from './lib/middleware'; +import { setupSavedObjects } from './saved_objects'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface PluginLegacyDependencies {} export type TaskManagerSetupContract = { - registerLegacyAPI: (legacyDependencies: PluginLegacyDependencies) => Promise; + registerLegacyAPI: () => Promise; } & Pick; export type TaskManagerStartContract = Pick< @@ -35,12 +34,18 @@ export class TaskManagerPlugin this.currentConfig = {} as TaskManagerConfig; } - public setup(core: CoreSetup, plugins: unknown): TaskManagerSetupContract { + public async setup(core: CoreSetup, plugins: unknown): Promise { const logger = this.initContext.logger.get('taskManager'); - const config$ = this.initContext.config.create(); + const config = await this.initContext.config + .create() + .pipe(first()) + .toPromise(); + + setupSavedObjects(core.savedObjects, config); + return { - registerLegacyAPI: once((__LEGACY: PluginLegacyDependencies) => { - config$.subscribe(async config => { + registerLegacyAPI: once(() => { + (async () => { const [{ savedObjects, elasticsearch }] = await core.getStartServices(); const savedObjectsRepository = savedObjects.createInternalRepository(['task']); this.legacyTaskManager$.next( @@ -53,7 +58,7 @@ export class TaskManagerPlugin }) ); this.legacyTaskManager$.complete(); - }); + })(); return this.taskManager; }), addMiddleware: (middleware: Middleware) => { diff --git a/x-pack/plugins/task_manager/server/saved_objects/index.ts b/x-pack/plugins/task_manager/server/saved_objects/index.ts new file mode 100644 index 0000000000000..0ad9021cd7f39 --- /dev/null +++ b/x-pack/plugins/task_manager/server/saved_objects/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 { SavedObjectsServiceSetup } from 'kibana/server'; +import mappings from './mappings.json'; +import { TaskManagerConfig } from '../config.js'; + +export function setupSavedObjects( + savedObjects: SavedObjectsServiceSetup, + config: TaskManagerConfig +) { + savedObjects.registerType({ + name: 'task', + namespaceType: 'agnostic', + hidden: true, + convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, + mappings: mappings.task, + indexPattern: config.index, + }); +} diff --git a/x-pack/legacy/plugins/task_manager/server/mappings.json b/x-pack/plugins/task_manager/server/saved_objects/mappings.json similarity index 100% rename from x-pack/legacy/plugins/task_manager/server/mappings.json rename to x-pack/plugins/task_manager/server/saved_objects/mappings.json diff --git a/x-pack/legacy/plugins/task_manager/server/migrations.ts b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts similarity index 100% rename from x-pack/legacy/plugins/task_manager/server/migrations.ts rename to x-pack/plugins/task_manager/server/saved_objects/migrations.ts diff --git a/x-pack/plugins/task_manager/server/task_manager.ts b/x-pack/plugins/task_manager/server/task_manager.ts index 24ceea0fe71ef..2a45a599120dd 100644 --- a/x-pack/plugins/task_manager/server/task_manager.ts +++ b/x-pack/plugins/task_manager/server/task_manager.ts @@ -240,7 +240,7 @@ export class TaskManager { * @param taskDefinitions - The Kibana task definitions dictionary */ public registerTaskDefinitions(taskDefinitions: TaskDictionary) { - this.assertUninitialized('register task definitions'); + this.assertUninitialized('register task definitions', Object.keys(taskDefinitions).join(', ')); const duplicate = Object.keys(taskDefinitions).find(k => !!this.definitions[k]); if (duplicate) { throw new Error(`Task ${duplicate} is already defined!`); @@ -360,9 +360,11 @@ export class TaskManager { * @param {string} message shown if task manager is already initialized * @returns void */ - private assertUninitialized(message: string) { + private assertUninitialized(message: string, context?: string) { if (this.isStarted) { - throw new Error(`Cannot ${message} after the task manager is initialized!`); + throw new Error( + `${context ? `[${context}] ` : ''}Cannot ${message} after the task manager is initialized` + ); } } } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index cdff34ec3a603..a3011ab5bdfa9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8225,11 +8225,8 @@ "xpack.ingestManager.agentConfigList.addButton": "エージェント構成を作成", "xpack.ingestManager.agentConfigList.agentsColumnTitle": "エージェント", "xpack.ingestManager.agentConfigList.clearFiltersLinkText": "フィルターを消去", - "xpack.ingestManager.agentConfigList.copyConfigActionText": "構成をコピー", "xpack.ingestManager.agentConfigList.createDatasourceActionText": "データソースを作成", "xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "データソース", - "xpack.ingestManager.agentConfigList.deleteButton": "{count, plural, one {# エージェント設定} other {# エージェント設定}}を削除", - "xpack.ingestManager.agentConfigList.deleteConfigActionText": "構成の削除", "xpack.ingestManager.agentConfigList.descriptionColumnTitle": "説明", "xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "エージェント構成を読み込み中...", "xpack.ingestManager.agentConfigList.nameColumnTitle": "名前", @@ -8313,7 +8310,6 @@ "xpack.ingestManager.configDetails.configDetailsTitle": "構成「{id}」", "xpack.ingestManager.configDetails.configNotFoundErrorTitle": "構成「{id}」が見つかりません", "xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "アクション", - "xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle": "データソースをコピー", "xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "データソースを削除", "xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "説明", "xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "データソースを編集", @@ -8321,7 +8317,6 @@ "xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "名前空間", "xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "パッケージ", "xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "ストリーム", - "xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle": "データソースを表示", "xpack.ingestManager.configDetails.subTabs.datasouces": "データソース", "xpack.ingestManager.configDetails.subTabs.settings": "設定", "xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML ファイル", @@ -10505,10 +10500,9 @@ "xpack.ml.overview.feedbackSectionTitle": "フィードバック", "xpack.ml.overview.gettingStartedSectionCreateJob": "新規ジョブを作成中", "xpack.ml.overview.gettingStartedSectionDocs": "ドキュメンテーション", - "xpack.ml.overview.gettingStartedSectionText": "機械学習へようこそ。はじめに{docs}や{createJob}をご参照ください。Elastic Stackの機械学習の詳細については、{whatIsMachineLearning}をご覧ください。{transforms}を使用して、分析ジョブの機能インデックスを作成することをお勧めします。", + "xpack.ml.overview.gettingStartedSectionText": "機械学習へようこそ。はじめに{docs}や{createJob}をご参照ください。{transforms}を使用して、分析ジョブの機能インデックスを作成することをお勧めします。", "xpack.ml.overview.gettingStartedSectionTitle": "はじめて使う", "xpack.ml.overview.gettingStartedSectionTransforms": "Elasticsearchの変換", - "xpack.ml.overview.gettingStartedSectionWhatIsMachineLearning": "こちら", "xpack.ml.overview.overviewLabel": "概要", "xpack.ml.overview.statsBar.failedAnalyticsLabel": "失敗", "xpack.ml.overview.statsBar.runningAnalyticsLabel": "実行中", @@ -15905,7 +15899,6 @@ "xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "アクションタイプ", "xpack.triggersActionsUI.sections.alertsList.addActionButtonLabel": "アラートの作成", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "タイプ", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editLinkTitle": "編集", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle": "次の間隔で実行", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名前", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "タグ", @@ -16112,7 +16105,6 @@ "xpack.uptime.emptyStateError.notAuthorized": "アップタイムデータの表示が承認されていません。システム管理者にお問い合わせください。", "xpack.uptime.emptyStateError.notFoundPage": "ページが見つかりません", "xpack.uptime.emptyStateError.title": "エラー", - "xpack.uptime.featureCatalogueDescription": "エンドポイントヘルスチェックとアップタイム監視を行います。", "xpack.uptime.featureRegistry.uptimeFeatureName": "アップタイム", "xpack.uptime.filterBar.ariaLabel": "概要ページのインプットフィルター基準", "xpack.uptime.filterBar.filterDownLabel": "ダウン", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 819112feb9f57..e373d05a7d851 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8228,11 +8228,8 @@ "xpack.ingestManager.agentConfigList.addButton": "创建代理配置", "xpack.ingestManager.agentConfigList.agentsColumnTitle": "代理", "xpack.ingestManager.agentConfigList.clearFiltersLinkText": "清除筛选", - "xpack.ingestManager.agentConfigList.copyConfigActionText": "复制配置", "xpack.ingestManager.agentConfigList.createDatasourceActionText": "创建数据源", "xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "数据源", - "xpack.ingestManager.agentConfigList.deleteButton": "删除 {count, plural, one {# 个代理配置} other {# 个代理配置}}", - "xpack.ingestManager.agentConfigList.deleteConfigActionText": "删除配置", "xpack.ingestManager.agentConfigList.descriptionColumnTitle": "描述", "xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "正在加载代理配置……", "xpack.ingestManager.agentConfigList.nameColumnTitle": "名称", @@ -8316,7 +8313,6 @@ "xpack.ingestManager.configDetails.configDetailsTitle": "配置“{id}”", "xpack.ingestManager.configDetails.configNotFoundErrorTitle": "未找到配置“{id}”", "xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "操作", - "xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle": "复制数据源", "xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "删除数据源", "xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "描述", "xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "编辑数据源", @@ -8324,7 +8320,6 @@ "xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "命名空间", "xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "软件包", "xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "流计数", - "xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle": "查看数据源", "xpack.ingestManager.configDetails.subTabs.datasouces": "数据源", "xpack.ingestManager.configDetails.subTabs.settings": "设置", "xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML 文件", @@ -10508,10 +10503,9 @@ "xpack.ml.overview.feedbackSectionTitle": "反馈", "xpack.ml.overview.gettingStartedSectionCreateJob": "创建新作业", "xpack.ml.overview.gettingStartedSectionDocs": "文档", - "xpack.ml.overview.gettingStartedSectionText": "欢迎使用 Machine Learning。首先阅读我们的{docs}或{createJob}。有关 Elastic Stack 中的机器学习的详情,请参阅{whatIsMachineLearning}。建议使用 {transforms}为分析作业创建功能索引。", + "xpack.ml.overview.gettingStartedSectionText": "欢迎使用 Machine Learning。首先阅读我们的{docs}或{createJob}。建议使用 {transforms}为分析作业创建功能索引。", "xpack.ml.overview.gettingStartedSectionTitle": "入门", "xpack.ml.overview.gettingStartedSectionTransforms": "Elasticsearch 的转换", - "xpack.ml.overview.gettingStartedSectionWhatIsMachineLearning": "此处", "xpack.ml.overview.overviewLabel": "概览", "xpack.ml.overview.statsBar.failedAnalyticsLabel": "失败", "xpack.ml.overview.statsBar.runningAnalyticsLabel": "正在运行", @@ -15910,7 +15904,6 @@ "xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "操作类型", "xpack.triggersActionsUI.sections.alertsList.addActionButtonLabel": "创建告警", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "类型", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editLinkTitle": "编辑", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle": "运行间隔", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名称", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "标记", @@ -16117,7 +16110,6 @@ "xpack.uptime.emptyStateError.notAuthorized": "您无权查看 Uptime 数据,请联系系统管理员。", "xpack.uptime.emptyStateError.notFoundPage": "未找到页面", "xpack.uptime.emptyStateError.title": "错误", - "xpack.uptime.featureCatalogueDescription": "执行终端节点运行状况检查和运行时间监测。", "xpack.uptime.featureRegistry.uptimeFeatureName": "运行时间", "xpack.uptime.filterBar.ariaLabel": "概览页面的输入筛选条件", "xpack.uptime.filterBar.filterDownLabel": "关闭", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index 72c22f46f217e..8406987e4ed9d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -190,5 +190,25 @@ describe('alert_form', () => { const alertTypeSelectOptions = wrapper.find('[data-test-subj="selectedAlertTypeTitle"]'); expect(alertTypeSelectOptions.exists()).toBeTruthy(); }); + + it('should update throttle value', async () => { + const newThrottle = 17; + await setup(); + const throttleField = wrapper.find('[data-test-subj="throttleInput"]'); + expect(throttleField.exists()).toBeTruthy(); + throttleField.at(1).simulate('change', { target: { value: newThrottle.toString() } }); + const throttleFieldAfterUpdate = wrapper.find('[data-test-subj="throttleInput"]'); + expect(throttleFieldAfterUpdate.at(1).prop('value')).toEqual(newThrottle); + }); + + it('should unset throttle value', async () => { + const newThrottle = ''; + await setup(); + const throttleField = wrapper.find('[data-test-subj="throttleInput"]'); + expect(throttleField.exists()).toBeTruthy(); + throttleField.at(1).simulate('change', { target: { value: newThrottle } }); + const throttleFieldAfterUpdate = wrapper.find('[data-test-subj="throttleInput"]'); + expect(throttleFieldAfterUpdate.at(1).prop('value')).toEqual(newThrottle); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 66aa02e1930a3..a51ebc3126785 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -245,10 +245,6 @@ describe('alerts_list component with items', () => { expect(wrapper.find('EuiBasicTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); }); - it('renders edit button for registered alert types', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="alertsTableCell-editLink"]').length).toBeGreaterThan(0); - }); }); describe('alerts_list component empty with show only capability', () => { @@ -442,8 +438,4 @@ describe('alerts_list with show only capability', () => { expect(wrapper.find('EuiTableRow')).toHaveLength(2); // TODO: check delete button }); - it('not renders edit button for non registered alert types', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="alertsTableCell-editLink"]').length).toBe(0); - }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 1103d7c3921a7..2d9cfcdbda89f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -24,7 +24,7 @@ import { isEmpty } from 'lodash'; import { AlertsContextProvider } from '../../../context/alerts_context'; import { useAppDependencies } from '../../../app_context'; import { ActionType, Alert, AlertTableItem, AlertTypeIndex, Pagination } from '../../../../types'; -import { AlertAdd, AlertEdit } from '../../alert_form'; +import { AlertAdd } from '../../alert_form'; import { BulkOperationPopover } from '../../common/components/bulk_operation_popover'; import { AlertQuickEditButtonsWithApi as AlertQuickEditButtons } from '../../common/components/alert_quick_edit_buttons'; import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions'; @@ -85,8 +85,6 @@ export const AlertsList: React.FunctionComponent = () => { data: [], totalItemCount: 0, }); - const [editedAlertItem, setEditedAlertItem] = useState(undefined); - const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const [alertsToDelete, setAlertsToDelete] = useState([]); useEffect(() => { @@ -162,11 +160,6 @@ export const AlertsList: React.FunctionComponent = () => { } } - async function editItem(alertTableItem: AlertTableItem) { - setEditedAlertItem(alertTableItem); - setEditFlyoutVisibility(true); - } - const alertsTableColumns = [ { field: 'name', @@ -219,27 +212,6 @@ export const AlertsList: React.FunctionComponent = () => { truncateText: false, 'data-test-subj': 'alertsTableCell-interval', }, - { - name: '', - width: '50px', - render(item: AlertTableItem) { - if (!canSave || !alertTypeRegistry.has(item.alertTypeId)) { - return; - } - return ( - editItem(item)} - > - - - ); - }, - }, { name: '', width: '40px', @@ -453,14 +425,6 @@ export const AlertsList: React.FunctionComponent = () => { addFlyoutVisible={alertFlyoutVisible} setAddFlyoutVisibility={setAlertFlyoutVisibility} /> - {editFlyoutVisible && editedAlertItem ? ( - - ) : null} ); diff --git a/x-pack/legacy/plugins/uptime/README.md b/x-pack/plugins/uptime/README.md similarity index 100% rename from x-pack/legacy/plugins/uptime/README.md rename to x-pack/plugins/uptime/README.md diff --git a/x-pack/legacy/plugins/uptime/common/constants/alerts.ts b/x-pack/plugins/uptime/common/constants/alerts.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/alerts.ts rename to x-pack/plugins/uptime/common/constants/alerts.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/capabilities.ts b/x-pack/plugins/uptime/common/constants/capabilities.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/capabilities.ts rename to x-pack/plugins/uptime/common/constants/capabilities.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/chart_format_limits.ts b/x-pack/plugins/uptime/common/constants/chart_format_limits.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/chart_format_limits.ts rename to x-pack/plugins/uptime/common/constants/chart_format_limits.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/client_defaults.ts b/x-pack/plugins/uptime/common/constants/client_defaults.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/client_defaults.ts rename to x-pack/plugins/uptime/common/constants/client_defaults.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts b/x-pack/plugins/uptime/common/constants/context_defaults.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts rename to x-pack/plugins/uptime/common/constants/context_defaults.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/index.ts b/x-pack/plugins/uptime/common/constants/index.ts similarity index 93% rename from x-pack/legacy/plugins/uptime/common/constants/index.ts rename to x-pack/plugins/uptime/common/constants/index.ts index 72d498056d6b3..00baa39044a55 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/index.ts +++ b/x-pack/plugins/uptime/common/constants/index.ts @@ -11,6 +11,6 @@ export { CONTEXT_DEFAULTS } from './context_defaults'; export * from './capabilities'; export * from './settings_defaults'; export { PLUGIN } from './plugin'; -export { QUERY, STATES } from './query'; +export { QUERY } from './query'; export * from './ui'; export * from './rest_api'; diff --git a/x-pack/legacy/plugins/uptime/common/constants/plugin.ts b/x-pack/plugins/uptime/common/constants/plugin.ts similarity index 64% rename from x-pack/legacy/plugins/uptime/common/constants/plugin.ts rename to x-pack/plugins/uptime/common/constants/plugin.ts index 00781726941d5..6064524872a0a 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/plugin.ts +++ b/x-pack/plugins/uptime/common/constants/plugin.ts @@ -8,12 +8,17 @@ import { i18n } from '@kbn/i18n'; export const PLUGIN = { APP_ROOT_ID: 'react-uptime-root', - DESCRIPTION: 'Uptime monitoring', + DESCRIPTION: i18n.translate('xpack.uptime.pluginDescription', { + defaultMessage: 'Uptime monitoring', + description: 'The description text that will appear in the feature catalogue.', + }), ID: 'uptime', LOCAL_STORAGE_KEY: 'xpack.uptime', NAME: i18n.translate('xpack.uptime.featureRegistry.uptimeFeatureName', { defaultMessage: 'Uptime', }), ROUTER_BASE_NAME: '/app/uptime#', - TITLE: 'uptime', + TITLE: i18n.translate('xpack.uptime.uptimeFeatureCatalogueTitle', { + defaultMessage: 'Uptime', + }), }; diff --git a/x-pack/legacy/plugins/uptime/common/constants/query.ts b/x-pack/plugins/uptime/common/constants/query.ts similarity index 78% rename from x-pack/legacy/plugins/uptime/common/constants/query.ts rename to x-pack/plugins/uptime/common/constants/query.ts index d728f114aae76..21574f1d8b27e 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/query.ts +++ b/x-pack/plugins/uptime/common/constants/query.ts @@ -25,10 +25,3 @@ export const QUERY = { 'error.type', ], }; - -export const STATES = { - // Number of results returned for a states query - LEGACY_STATES_QUERY_SIZE: 10, - // The maximum number of monitors that should be supported - MAX_MONITORS: 35000, -}; diff --git a/x-pack/legacy/plugins/uptime/common/constants/rest_api.ts b/x-pack/plugins/uptime/common/constants/rest_api.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/rest_api.ts rename to x-pack/plugins/uptime/common/constants/rest_api.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/settings_defaults.ts b/x-pack/plugins/uptime/common/constants/settings_defaults.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/settings_defaults.ts rename to x-pack/plugins/uptime/common/constants/settings_defaults.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/ui.ts b/x-pack/plugins/uptime/common/constants/ui.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/ui.ts rename to x-pack/plugins/uptime/common/constants/ui.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/alerts/index.ts b/x-pack/plugins/uptime/common/runtime_types/alerts/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/alerts/index.ts rename to x-pack/plugins/uptime/common/runtime_types/alerts/index.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/alerts/status_check.ts b/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/alerts/status_check.ts rename to x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/certs.ts b/x-pack/plugins/uptime/common/runtime_types/certs.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/certs.ts rename to x-pack/plugins/uptime/common/runtime_types/certs.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/common.ts b/x-pack/plugins/uptime/common/runtime_types/common.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/common.ts rename to x-pack/plugins/uptime/common/runtime_types/common.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts b/x-pack/plugins/uptime/common/runtime_types/dynamic_settings.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts rename to x-pack/plugins/uptime/common/runtime_types/dynamic_settings.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts b/x-pack/plugins/uptime/common/runtime_types/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/index.ts rename to x-pack/plugins/uptime/common/runtime_types/index.ts index 78aab3806ae04..e80471bf8b56f 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts +++ b/x-pack/plugins/uptime/common/runtime_types/index.ts @@ -7,8 +7,8 @@ export * from './alerts'; export * from './certs'; export * from './common'; +export * from './dynamic_settings'; export * from './monitor'; export * from './overview_filters'; export * from './ping'; export * from './snapshot'; -export * from './dynamic_settings'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/details.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/details.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/monitor/details.ts rename to x-pack/plugins/uptime/common/runtime_types/monitor/details.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/index.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/monitor/index.ts rename to x-pack/plugins/uptime/common/runtime_types/monitor/index.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/locations.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/locations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/monitor/locations.ts rename to x-pack/plugins/uptime/common/runtime_types/monitor/locations.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/state.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/monitor/state.ts rename to x-pack/plugins/uptime/common/runtime_types/monitor/state.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/index.ts b/x-pack/plugins/uptime/common/runtime_types/overview_filters/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/index.ts rename to x-pack/plugins/uptime/common/runtime_types/overview_filters/index.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts b/x-pack/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts rename to x-pack/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/ping/histogram.ts b/x-pack/plugins/uptime/common/runtime_types/ping/histogram.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/ping/histogram.ts rename to x-pack/plugins/uptime/common/runtime_types/ping/histogram.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/ping/index.ts b/x-pack/plugins/uptime/common/runtime_types/ping/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/ping/index.ts rename to x-pack/plugins/uptime/common/runtime_types/ping/index.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/ping/ping.ts rename to x-pack/plugins/uptime/common/runtime_types/ping/ping.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/index.ts b/x-pack/plugins/uptime/common/runtime_types/snapshot/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/index.ts rename to x-pack/plugins/uptime/common/runtime_types/snapshot/index.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts b/x-pack/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts rename to x-pack/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts diff --git a/x-pack/legacy/plugins/uptime/common/types/index.ts b/x-pack/plugins/uptime/common/types/index.ts similarity index 69% rename from x-pack/legacy/plugins/uptime/common/types/index.ts rename to x-pack/plugins/uptime/common/types/index.ts index a32eabd49a3e5..71ccd54dd3cdc 100644 --- a/x-pack/legacy/plugins/uptime/common/types/index.ts +++ b/x-pack/plugins/uptime/common/types/index.ts @@ -4,18 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -/** Represents a bucket of monitor status information. */ -export interface StatusData { - /** The timeseries point for this status data. */ - x: number; - /** The value of up counts for this point. */ - up?: number | null; - /** The value for down counts for this point. */ - down?: number | null; - /** The total down counts for this point. */ - total?: number | null; -} - /** Represents the average monitor duration ms at a point in time. */ export interface MonitorDurationAveragePoint { /** The timeseries value for this point. */ diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index 6ec8a755ebea0..ce8b64ce07254 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -2,8 +2,16 @@ "configPath": ["xpack", "uptime"], "id": "uptime", "kibanaVersion": "kibana", - "requiredPlugins": ["alerting", "features", "licensing", "usageCollection"], + "optionalPlugins": ["capabilities", "data", "home"], + "requiredPlugins": [ + "alerting", + "embeddable", + "features", + "licensing", + "triggers_actions_ui", + "usageCollection" + ], "server": true, - "ui": false, + "ui": true, "version": "8.0.0" } diff --git a/x-pack/legacy/plugins/uptime/public/app.ts b/x-pack/plugins/uptime/public/app.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/app.ts rename to x-pack/plugins/uptime/public/app.ts diff --git a/x-pack/plugins/uptime/public/apps/index.ts b/x-pack/plugins/uptime/public/apps/index.ts new file mode 100644 index 0000000000000..65b80d08d4f20 --- /dev/null +++ b/x-pack/plugins/uptime/public/apps/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 { UptimePlugin } from './plugin'; diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts new file mode 100644 index 0000000000000..719dac022dada --- /dev/null +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -0,0 +1,72 @@ +/* + * 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 { LegacyCoreStart, AppMountParameters } from 'src/core/public'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; +import { UMFrontendLibs } from '../lib/lib'; +import { PLUGIN } from '../../common/constants'; +import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; +import { getKibanaFrameworkAdapter } from '../lib/adapters/framework/new_platform_adapter'; +import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; +import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public'; +import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; + +export interface StartObject { + core: LegacyCoreStart; + plugins: any; +} + +export interface ClientPluginsSetup { + data: DataPublicPluginSetup; + home: HomePublicPluginSetup; + triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; +} + +export interface ClientPluginsStart { + embeddable: EmbeddableStart; +} + +export class UptimePlugin implements Plugin { + constructor(_context: PluginInitializerContext) {} + + public async setup( + core: CoreSetup, + plugins: ClientPluginsSetup + ): Promise { + if (plugins.home) { + plugins.home.featureCatalogue.register({ + id: PLUGIN.ID, + title: PLUGIN.TITLE, + description: PLUGIN.DESCRIPTION, + icon: 'uptimeApp', + path: '/app/uptime#/', + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + }); + } + + core.application.register({ + appRoute: '/app/uptime#/', + id: PLUGIN.ID, + euiIconType: 'uptimeApp', + order: 8900, + title: PLUGIN.TITLE, + async mount(params: AppMountParameters) { + const [coreStart, corePlugins] = await core.getStartServices(); + const { element } = params; + const libs: UMFrontendLibs = { + framework: getKibanaFrameworkAdapter(coreStart, plugins, corePlugins), + }; + libs.framework.render(element); + return () => {}; + }, + }); + } + + public start(_start: CoreStart, _plugins: {}): void {} + + public stop(): void {} +} diff --git a/x-pack/legacy/plugins/uptime/public/apps/template.html b/x-pack/plugins/uptime/public/apps/template.html similarity index 100% rename from x-pack/legacy/plugins/uptime/public/apps/template.html rename to x-pack/plugins/uptime/public/apps/template.html diff --git a/x-pack/legacy/plugins/uptime/public/badge.ts b/x-pack/plugins/uptime/public/badge.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/badge.ts rename to x-pack/plugins/uptime/public/badge.ts diff --git a/x-pack/legacy/plugins/uptime/public/breadcrumbs.ts b/x-pack/plugins/uptime/public/breadcrumbs.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/breadcrumbs.ts rename to x-pack/plugins/uptime/public/breadcrumbs.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/common/__tests__/__snapshots__/location_link.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/location_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/__tests__/__snapshots__/location_link.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/location_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/__tests__/location_link.test.tsx b/x-pack/plugins/uptime/public/components/common/__tests__/location_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/__tests__/location_link.test.tsx rename to x-pack/plugins/uptime/public/components/common/__tests__/location_link.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx b/x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx rename to x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_empty_state.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_empty_state.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_empty_state.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_empty_state.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/chart_empty_state.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/chart_empty_state.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/chart_empty_state.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/chart_empty_state.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/chart_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/chart_wrapper.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/chart_wrapper.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/chart_wrapper.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend_row.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend_row.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend_row.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend_row.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/duration_charts.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/duration_charts.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/duration_charts.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/duration_charts.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/get_tick_format.test.ts b/x-pack/plugins/uptime/public/components/common/charts/__tests__/get_tick_format.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/get_tick_format.test.ts rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/get_tick_format.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/monitor_bar_series.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/monitor_bar_series.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/monitor_bar_series.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/monitor_bar_series.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/ping_histogram.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/ping_histogram.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/ping_histogram.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/ping_histogram.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/annotation_tooltip.tsx b/x-pack/plugins/uptime/public/components/common/charts/annotation_tooltip.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/annotation_tooltip.tsx rename to x-pack/plugins/uptime/public/components/common/charts/annotation_tooltip.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/chart_empty_state.tsx b/x-pack/plugins/uptime/public/components/common/charts/chart_empty_state.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/chart_empty_state.tsx rename to x-pack/plugins/uptime/public/components/common/charts/chart_empty_state.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/chart_wrapper/chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/common/charts/chart_wrapper/chart_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/chart_wrapper/chart_wrapper.tsx rename to x-pack/plugins/uptime/public/components/common/charts/chart_wrapper/chart_wrapper.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/chart_wrapper/index.ts b/x-pack/plugins/uptime/public/components/common/charts/chart_wrapper/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/chart_wrapper/index.ts rename to x-pack/plugins/uptime/public/components/common/charts/chart_wrapper/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart.tsx b/x-pack/plugins/uptime/public/components/common/charts/donut_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart.tsx rename to x-pack/plugins/uptime/public/components/common/charts/donut_chart.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx b/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx rename to x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart_legend_row.tsx b/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend_row.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart_legend_row.tsx rename to x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend_row.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_chart.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/duration_chart.tsx rename to x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx similarity index 94% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx rename to x-pack/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx index a31a143b71fd2..ceb1e700f293e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx @@ -9,11 +9,11 @@ import moment from 'moment'; import { AnnotationTooltipFormatter, RectAnnotation } from '@elastic/charts'; import { RectAnnotationDatum } from '@elastic/charts/dist/chart_types/xy_chart/utils/specs'; import { AnnotationTooltip } from './annotation_tooltip'; -import { ANOMALY_SEVERITY } from '../../../../../../../plugins/ml/common/constants/anomalies'; +import { ANOMALY_SEVERITY } from '../../../../../../plugins/ml/common/constants/anomalies'; import { getSeverityColor, getSeverityType, -} from '../../../../../../../plugins/ml/common/util/anomaly_utils'; +} from '../../../../../../plugins/ml/common/util/anomaly_utils'; interface Props { anomalies: any; diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx rename to x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/get_tick_format.ts b/x-pack/plugins/uptime/public/components/common/charts/get_tick_format.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/get_tick_format.ts rename to x-pack/plugins/uptime/public/components/common/charts/get_tick_format.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/index.ts b/x-pack/plugins/uptime/public/components/common/charts/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/index.ts rename to x-pack/plugins/uptime/public/components/common/charts/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/monitor_bar_series.tsx b/x-pack/plugins/uptime/public/components/common/charts/monitor_bar_series.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/monitor_bar_series.tsx rename to x-pack/plugins/uptime/public/components/common/charts/monitor_bar_series.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/ping_histogram.tsx rename to x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/responsive_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/common/higher_order/__tests__/responsive_wrapper.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/responsive_wrapper.test.tsx rename to x-pack/plugins/uptime/public/components/common/higher_order/__tests__/responsive_wrapper.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/higher_order/index.ts b/x-pack/plugins/uptime/public/components/common/higher_order/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/higher_order/index.ts rename to x-pack/plugins/uptime/public/components/common/higher_order/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx b/x-pack/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx rename to x-pack/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/location_link.tsx b/x-pack/plugins/uptime/public/components/common/location_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/location_link.tsx rename to x-pack/plugins/uptime/public/components/common/location_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/uptime_date_picker.tsx b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/uptime_date_picker.tsx rename to x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/__snapshots__/monitor_charts.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/__tests__/__snapshots__/monitor_charts.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/__snapshots__/monitor_charts.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/__tests__/__snapshots__/monitor_charts.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/monitor_charts.test.tsx b/x-pack/plugins/uptime/public/components/monitor/__tests__/monitor_charts.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/monitor_charts.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/__tests__/monitor_charts.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/index.ts b/x-pack/plugins/uptime/public/components/monitor/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/index.ts rename to x-pack/plugins/uptime/public/components/monitor/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx similarity index 85% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx index 85d0b1b593704..06cdb07bd8bcd 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx @@ -7,21 +7,17 @@ import React, { useEffect, useState, useContext, useRef } from 'react'; import uuid from 'uuid'; import styled from 'styled-components'; -import { npStart } from 'ui/new_platform'; - -import { - ViewMode, - EmbeddableOutput, - ErrorEmbeddable, - isErrorEmbeddable, -} from '../../../../../../../../../src/plugins/embeddable/public'; +import { MapEmbeddable, MapEmbeddableInput } from '../../../../../../../legacy/plugins/maps/public'; import * as i18n from './translations'; -import { MapEmbeddable, MapEmbeddableInput } from '../../../../../../maps/public'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../../../plugins/maps/public'; import { Location } from '../../../../../common/runtime_types'; - import { getLayerList } from './map_config'; -import { UptimeThemeContext } from '../../../../contexts'; +import { UptimeThemeContext, UptimeStartupPluginsContext } from '../../../../contexts'; +import { + isErrorEmbeddable, + ViewMode, + ErrorEmbeddable, +} from '../../../../../../../../src/plugins/embeddable/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/public'; export interface EmbeddedMapProps { upPoints: LocationPoint[]; @@ -52,11 +48,11 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp const { colors } = useContext(UptimeThemeContext); const [embeddable, setEmbeddable] = useState(); const embeddableRoot: React.RefObject = useRef(null); - const factory = npStart.plugins.embeddable.getEmbeddableFactory< - MapEmbeddableInput, - EmbeddableOutput, - MapEmbeddable - >(MAP_SAVED_OBJECT_TYPE); + const { embeddable: embeddablePlugin } = useContext(UptimeStartupPluginsContext); + if (!embeddablePlugin) { + throw new Error('Embeddable start plugin not found'); + } + const factory: any = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); const input: MapEmbeddableInput = { id: uuid.v4(), @@ -88,7 +84,7 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp if (!factory) { throw new Error('Map embeddable not found.'); } - const embeddableObject = await factory.create({ + const embeddableObject: any = await factory.create({ ...input, title: i18n.MAP_TITLE, }); diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/index.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/index.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_map.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/location_map.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_map.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/location_map.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_missing.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/location_missing.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_missing.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/location_missing.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/license_info.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/license_info.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/license_info.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/license_info.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/confirm_delete.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/confirm_delete.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/confirm_delete.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/confirm_delete.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/license_info.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/license_info.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/license_info.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/license_info.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx index c0b02181dcce1..31cdcfac9feef 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx @@ -9,7 +9,7 @@ import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { MLFlyoutView } from '../ml_flyout'; import { UptimeSettingsContext } from '../../../../contexts'; import { CLIENT_DEFAULTS } from '../../../../../common/constants'; -import { License } from '../../../../../../../../plugins/licensing/common/license'; +import { License } from '../../../../../../../plugins/licensing/common/license'; const expiredLicense = new License({ signature: 'test signature', diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/index.ts b/x-pack/plugins/uptime/public/components/monitor/ml/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/index.ts rename to x-pack/plugins/uptime/public/components/monitor/ml/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/license_info.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/license_info.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/license_info.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/license_info.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx similarity index 98% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx index c3e8579ca4837..6eec30d405f76 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx @@ -19,7 +19,7 @@ import * as labels from './translations'; import { useKibana, KibanaReactNotifications, -} from '../../../../../../../../src/plugins/kibana_react/public'; +} from '../../../../../../../src/plugins/kibana_react/public'; import { MLFlyoutView } from './ml_flyout'; import { ML_JOB_ID } from '../../../../common/constants'; import { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts'; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx index 4963a901f0ecc..7f19885c15406 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx @@ -18,9 +18,9 @@ import { ConfirmJobDeletion } from './confirm_delete'; import { UptimeRefreshContext } from '../../../contexts'; import { getMLJobId } from '../../../state/api/ml_anomaly'; import * as labels from './translations'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ManageMLJobComponent } from './manage_ml_job'; -import { JobStat } from '../../../../../../../plugins/ml/common/types/data_recognizer'; +import { JobStat } from '../../../../../../plugins/ml/common/types/data_recognizer'; import { useMonitorId } from '../../../hooks'; export const MLIntegrationComponent = () => { diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/translations.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/translations.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_charts.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_charts.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_charts.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_charts.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/index.ts b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/index.ts rename to x-pack/plugins/uptime/public/components/monitor/monitor_duration/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx similarity index 96% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx index 7e39b977f1271..52d4f620f84b3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx @@ -20,7 +20,7 @@ import { } from '../../../state/selectors'; import { UptimeRefreshContext } from '../../../contexts'; import { getMLJobId } from '../../../state/api/ml_anomaly'; -import { JobStat } from '../../../../../../../plugins/ml/common/types/data_recognizer'; +import { JobStat } from '../../../../../ml/common/types/data_recognizer'; import { MonitorDurationComponent } from './monitor_duration'; import { MonitorIdParam } from '../../../../common/types'; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/status_by_location.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/status_by_location.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/status_by_location.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/status_by_location.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/status_by_location.test.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/status_by_location.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/status_by_location.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/status_by_location.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/index.ts b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/index.ts rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/index.ts b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/index.ts rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_by_location.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_by_location.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_by_location.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_by_location.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/translations.ts b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/translations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/translations.ts rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/translations.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/status_details.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/status_details.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/status_details.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/status_details.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/status_details_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/status_details_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/status_details_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/status_details_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/translations.ts b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/translations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/translations.ts rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/translations.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_histogram/index.ts b/x-pack/plugins/uptime/public/components/monitor/ping_histogram/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_histogram/index.ts rename to x-pack/plugins/uptime/public/components/monitor/ping_histogram/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/doc_link_body.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/doc_link_body.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/doc_link_body.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/doc_link_body.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/doc_link_body.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/doc_link_body.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/doc_link_body.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/doc_link_body.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/expanded_row.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/expanded_row.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/expanded_row.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/expanded_row.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_list.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_list.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_list.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_list.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/doc_link_body.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/doc_link_body.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/doc_link_body.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/doc_link_body.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/index.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/index.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/index.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/index.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/location_name.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/location_name.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/location_name.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/location_name.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/ping_list_container.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/ping_list_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/parsing_error_callout.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/parsing_error_callout.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/parsing_error_callout.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/parsing_error_callout.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot_heading.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot_heading.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot_heading.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot_heading.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/parsing_error_callout.test.tsx b/x-pack/plugins/uptime/public/components/overview/__tests__/parsing_error_callout.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/__tests__/parsing_error_callout.test.tsx rename to x-pack/plugins/uptime/public/components/overview/__tests__/parsing_error_callout.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/snapshot.test.tsx b/x-pack/plugins/uptime/public/components/overview/__tests__/snapshot.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/__tests__/snapshot.test.tsx rename to x-pack/plugins/uptime/public/components/overview/__tests__/snapshot.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/snapshot_heading.test.tsx b/x-pack/plugins/uptime/public/components/overview/__tests__/snapshot_heading.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/__tests__/snapshot_heading.test.tsx rename to x-pack/plugins/uptime/public/components/overview/__tests__/snapshot_heading.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/index.ts b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/index.ts rename to x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/uptime_alerts_flyout_wrapper.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/uptime_alerts_flyout_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/uptime_alerts_flyout_wrapper.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/uptime_alerts_flyout_wrapper.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/index.ts b/x-pack/plugins/uptime/public/components/overview/alerts/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/index.ts rename to x-pack/plugins/uptime/public/components/overview/alerts/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx index 04dfe4b3e3509..92fc015a276d3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx @@ -8,7 +8,7 @@ import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } f import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; interface Props { setAlertFlyoutVisible: (value: boolean) => void; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx similarity index 83% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx index 09e6dc72b7f98..262e1552e3660 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx @@ -5,8 +5,8 @@ */ import React from 'react'; -import { AlertsContextProvider } from '../../../../../../../plugins/triggers_actions_ui/public'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { AlertsContextProvider } from '../../../../../../plugins/triggers_actions_ui/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; export const UptimeAlertsContextProvider: React.FC = ({ children }) => { const { diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx similarity index 90% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx index 13705e7d19293..9b1d3a73dc661 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { AlertAdd } from '../../../../../../../plugins/triggers_actions_ui/public'; +import { AlertAdd } from '../../../../../../plugins/triggers_actions_ui/public'; interface Props { alertFlyoutVisible: boolean; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/data_or_index_missing.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/data_or_index_missing.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/data_or_index_missing.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/data_or_index_missing.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/data_or_index_missing.test.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/data_or_index_missing.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/data_or_index_missing.test.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/data_or_index_missing.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.tsx similarity index 96% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.tsx index 651103a34bf21..d38f203739cea 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state.tsx +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.tsx @@ -10,7 +10,7 @@ import { EmptyStateError } from './empty_state_error'; import { EmptyStateLoading } from './empty_state_loading'; import { DataOrIndexMissing } from './data_or_index_missing'; import { DynamicSettings, StatesIndexStatus } from '../../../../common/runtime_types'; -import { IHttpFetchError } from '../../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; interface EmptyStateProps { children: JSX.Element[] | JSX.Element; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx index 1135b969018a1..aa4040e319e0f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx @@ -7,7 +7,7 @@ import { EuiEmptyPrompt, EuiPanel, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; -import { IHttpFetchError } from '../../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; interface EmptyStateErrorProps { errors: IHttpFetchError[]; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_loading.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_loading.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_loading.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_loading.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/index.ts b/x-pack/plugins/uptime/public/components/overview/empty_state/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/index.ts rename to x-pack/plugins/uptime/public/components/overview/empty_state/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_popover.test.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/filter_popover.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_popover.test.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/filter_popover.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_status_button.test.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/filter_status_button.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_status_button.test.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/filter_status_button.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/parse_filter_map.test.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/parse_filter_map.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/parse_filter_map.test.ts rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/parse_filter_map.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/toggle_selected_item.test.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/toggle_selected_item.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/toggle_selected_item.test.ts rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/toggle_selected_item.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_status_button.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_status_button.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_status_button.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/filter_status_button.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/index.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/index.ts rename to x-pack/plugins/uptime/public/components/overview/filter_group/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts rename to x-pack/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/toggle_selected_item.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/toggle_selected_item.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/toggle_selected_item.ts rename to x-pack/plugins/uptime/public/components/overview/filter_group/toggle_selected_item.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/uptime_filter_button.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/uptime_filter_button.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/uptime_filter_button.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/uptime_filter_button.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/index.ts b/x-pack/plugins/uptime/public/components/overview/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/index.ts rename to x-pack/plugins/uptime/public/components/overview/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/index.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/index.ts rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx similarity index 98% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx index 63aceed2be636..792fff4c7cdca 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx @@ -16,7 +16,7 @@ import { IIndexPattern, QuerySuggestion, DataPublicPluginSetup, -} from '../../../../../../../../src/plugins/data/public'; +} from '../../../../../../../src/plugins/data/public'; const Container = styled.div` margin-bottom: 10px; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar_container.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar_container.tsx rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/click_outside.js b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/click_outside.js similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/click_outside.js rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/click_outside.js diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.d.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.d.ts rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.d.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.js b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.js similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.js rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.js diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js index 2b5ad9b59e39f..936eae04ffa64 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js @@ -14,7 +14,7 @@ import { units, fontSizes, unit, -} from '../../../../../../apm/public/style/variables'; +} from '../../../../../../../legacy/plugins/apm/public/style/variables'; import { tint } from 'polished'; import theme from '@elastic/eui/dist/eui_theme_light.json'; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js index 7fbabe71bcdb5..ac6832050b9d3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js @@ -9,7 +9,7 @@ import PropTypes from 'prop-types'; import styled from 'styled-components'; import { isEmpty } from 'lodash'; import Suggestion from './suggestion'; -import { units, px, unit } from '../../../../../../apm/public/style/variables'; +import { units, px, unit } from '../../../../../../../legacy/plugins/apm/public/style/variables'; import { tint } from 'polished'; import theme from '@elastic/eui/dist/eui_theme_light.json'; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_page_size_select.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_page_size_select.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_page_size_select.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_page_size_select.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_status_column.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_status_column.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_status_column.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_status_column.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_page_link.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_page_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_page_link.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_page_link.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/parse_timestamp.test.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/parse_timestamp.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/parse_timestamp.test.ts rename to x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/parse_timestamp.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/index.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/index.ts rename to x-pack/plugins/uptime/public/components/overview/monitor_list/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx similarity index 96% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx index 18e2e2437e147..7e9536689470e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx @@ -202,13 +202,7 @@ export const MonitorListComponent: React.FC = ({ itemId="monitor_id" itemIdToExpandedRowMap={getExpandedRowMap()} items={items} - // TODO: not needed without sorting and pagination - // onChange={onChange} noItemsMessage={!!filters ? labels.NO_MONITOR_ITEM_SELECTED : labels.NO_DATA_MESSAGE} - // TODO: reintegrate pagination in future release - // pagination={pagination} - // TODO: reintegrate sorting in future release - // sorting={sorting} columns={columns} /> diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx index 5bfe6ff0c5b4f..6fb880e28c734 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx @@ -9,7 +9,7 @@ import { useSelector, useDispatch } from 'react-redux'; import { getMonitorList } from '../../../state/actions'; import { FetchMonitorStatesQueryArgs } from '../../../../common/runtime_types'; import { monitorListSelector } from '../../../state/selectors'; -import { MonitorListComponent } from './index'; +import { MonitorListComponent } from './monitor_list'; export interface MonitorListProps { filters?: string; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover_container.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover_container.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_link.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_link.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/index.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/index.ts rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_row.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_row.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_row.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_row.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_page_size_select.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_page_size_select.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_page_size_select.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_page_size_select.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_page_link.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_page_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_page_link.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_page_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/overview_page_link.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/overview_page_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/overview_page_link.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/overview_page_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/parse_timestamp.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/parse_timestamp.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/parse_timestamp.ts rename to x-pack/plugins/uptime/public/components/overview/monitor_list/parse_timestamp.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/translations.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/translations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/translations.ts rename to x-pack/plugins/uptime/public/components/overview/monitor_list/translations.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/types.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/types.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/types.ts rename to x-pack/plugins/uptime/public/components/overview/monitor_list/types.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/overview_container.tsx b/x-pack/plugins/uptime/public/components/overview/overview_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/overview_container.tsx rename to x-pack/plugins/uptime/public/components/overview/overview_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/parsing_error_callout.tsx b/x-pack/plugins/uptime/public/components/overview/parsing_error_callout.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/parsing_error_callout.tsx rename to x-pack/plugins/uptime/public/components/overview/parsing_error_callout.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/index.ts b/x-pack/plugins/uptime/public/components/overview/snapshot/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/snapshot/index.ts rename to x-pack/plugins/uptime/public/components/overview/snapshot/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot.tsx b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot.tsx rename to x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx rename to x-pack/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot_heading.tsx b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot_heading.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot_heading.tsx rename to x-pack/plugins/uptime/public/components/overview/snapshot/snapshot_heading.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/status_panel.tsx b/x-pack/plugins/uptime/public/components/overview/status_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/status_panel.tsx rename to x-pack/plugins/uptime/public/components/overview/status_panel.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap b/x-pack/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap rename to x-pack/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap b/x-pack/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap rename to x-pack/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx b/x-pack/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx rename to x-pack/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx b/x-pack/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx rename to x-pack/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx b/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx rename to x-pack/plugins/uptime/public/components/settings/certificate_form.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx b/x-pack/plugins/uptime/public/components/settings/indices_form.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx rename to x-pack/plugins/uptime/public/components/settings/indices_form.tsx diff --git a/x-pack/legacy/plugins/uptime/public/contexts/index.ts b/x-pack/plugins/uptime/public/contexts/index.ts similarity index 82% rename from x-pack/legacy/plugins/uptime/public/contexts/index.ts rename to x-pack/plugins/uptime/public/contexts/index.ts index 2b27fcfe907ab..243a25c26901a 100644 --- a/x-pack/legacy/plugins/uptime/public/contexts/index.ts +++ b/x-pack/plugins/uptime/public/contexts/index.ts @@ -11,3 +11,7 @@ export { UptimeSettingsContextProvider, } from './uptime_settings_context'; export { UptimeThemeContextProvider, UptimeThemeContext } from './uptime_theme_context'; +export { + UptimeStartupPluginsContext, + UptimeStartupPluginsContextProvider, +} from './uptime_startup_plugins_context'; diff --git a/x-pack/legacy/plugins/uptime/public/contexts/uptime_refresh_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_refresh_context.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/contexts/uptime_refresh_context.tsx rename to x-pack/plugins/uptime/public/contexts/uptime_refresh_context.tsx diff --git a/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_settings_context.tsx similarity index 96% rename from x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx rename to x-pack/plugins/uptime/public/contexts/uptime_settings_context.tsx index 137846de103b4..4fabf3f2ed497 100644 --- a/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx +++ b/x-pack/plugins/uptime/public/contexts/uptime_settings_context.tsx @@ -9,7 +9,7 @@ import { UptimeAppProps } from '../uptime_app'; import { CLIENT_DEFAULTS, CONTEXT_DEFAULTS } from '../../common/constants'; import { CommonlyUsedRange } from '../components/common/uptime_date_picker'; import { useGetUrlParams } from '../hooks'; -import { ILicense } from '../../../../../plugins/licensing/common/types'; +import { ILicense } from '../../../../plugins/licensing/common/types'; export interface UptimeSettingsContextValues { basePath: string; diff --git a/x-pack/plugins/uptime/public/contexts/uptime_startup_plugins_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_startup_plugins_context.tsx new file mode 100644 index 0000000000000..e516ff44aa12a --- /dev/null +++ b/x-pack/plugins/uptime/public/contexts/uptime_startup_plugins_context.tsx @@ -0,0 +1,15 @@ +/* + * 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, { createContext } from 'react'; +import { ClientPluginsStart } from '../apps/plugin'; + +export const UptimeStartupPluginsContext = createContext>({}); + +export const UptimeStartupPluginsContextProvider: React.FC> = ({ + children, + ...props +}) => ; diff --git a/x-pack/legacy/plugins/uptime/public/contexts/uptime_theme_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/contexts/uptime_theme_context.tsx rename to x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap b/x-pack/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap rename to x-pack/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx b/x-pack/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx rename to x-pack/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx index 1ce00fe7ce3af..306919015fcb1 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx +++ b/x-pack/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Route } from 'react-router-dom'; import { mountWithRouter } from '../../lib'; import { OVERVIEW_ROUTE } from '../../../common/constants'; -import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { UptimeUrlParams, getSupportedUrlParams } from '../../lib/helper'; import { makeBaseBreadcrumb, useBreadcrumbs } from '../use_breadcrumbs'; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx b/x-pack/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx rename to x-pack/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/hooks/index.ts b/x-pack/plugins/uptime/public/hooks/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/hooks/index.ts rename to x-pack/plugins/uptime/public/hooks/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts b/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts similarity index 95% rename from x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts rename to x-pack/plugins/uptime/public/hooks/update_kuery_string.ts index ab4d6f75849e8..492d2bab5bb80 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts +++ b/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts @@ -5,7 +5,7 @@ */ import { combineFiltersAndUserSearch, stringifyKueries } from '../lib/helper'; -import { esKuery, IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { esKuery, IIndexPattern } from '../../../../../src/plugins/data/public'; const getKueryString = (urlFilters: string): string => { let kueryString = ''; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/uptime/public/hooks/use_breadcrumbs.ts similarity index 94% rename from x-pack/legacy/plugins/uptime/public/hooks/use_breadcrumbs.ts rename to x-pack/plugins/uptime/public/hooks/use_breadcrumbs.ts index d1cc8e1897386..182c6b0114128 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/uptime/public/hooks/use_breadcrumbs.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { useEffect } from 'react'; import { UptimeUrlParams } from '../lib/helper'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { useUrlParams } from '.'; export const makeBaseBreadcrumb = (params?: UptimeUrlParams): ChromeBreadcrumb => { diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_monitor.ts b/x-pack/plugins/uptime/public/hooks/use_monitor.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/hooks/use_monitor.ts rename to x-pack/plugins/uptime/public/hooks/use_monitor.ts diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts rename to x-pack/plugins/uptime/public/hooks/use_telemetry.ts diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts b/x-pack/plugins/uptime/public/hooks/use_url_params.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts rename to x-pack/plugins/uptime/public/hooks/use_url_params.ts diff --git a/x-pack/legacy/plugins/uptime/public/icons/heartbeat_white.svg b/x-pack/plugins/uptime/public/icons/heartbeat_white.svg similarity index 100% rename from x-pack/legacy/plugins/uptime/public/icons/heartbeat_white.svg rename to x-pack/plugins/uptime/public/icons/heartbeat_white.svg diff --git a/x-pack/plugins/uptime/public/index.ts b/x-pack/plugins/uptime/public/index.ts new file mode 100644 index 0000000000000..48cf2c90ad07b --- /dev/null +++ b/x-pack/plugins/uptime/public/index.ts @@ -0,0 +1,11 @@ +/* + * 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 'kibana/public'; +import { UptimePlugin } from './apps'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new UptimePlugin(initializerContext); diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts b/x-pack/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts rename to x-pack/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx b/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx similarity index 86% rename from x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx rename to x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx index 71c73bf5ba5d4..f7f9e056f41af 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx +++ b/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx @@ -9,7 +9,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { get } from 'lodash'; import { i18n as i18nFormatter } from '@kbn/i18n'; -import { PluginsSetup } from 'ui/new_platform/new_platform'; import { alertTypeInitializers } from '../../alert_types'; import { UptimeApp, UptimeAppProps } from '../../../uptime_app'; import { getIntegratedAppAvailability } from './capabilities_adapter'; @@ -20,10 +19,12 @@ import { DEFAULT_TIMEPICKER_QUICK_RANGES, } from '../../../../common/constants'; import { UMFrameworkAdapter } from '../../lib'; +import { ClientPluginsStart, ClientPluginsSetup } from '../../../apps/plugin'; export const getKibanaFrameworkAdapter = ( core: CoreStart, - plugins: PluginsSetup + plugins: ClientPluginsSetup, + startPlugins: ClientPluginsStart ): UMFrameworkAdapter => { const { application: { capabilities }, @@ -35,14 +36,15 @@ export const getKibanaFrameworkAdapter = ( const { data: { autocomplete }, - // TODO: after NP migration we can likely fix this typing problem - // @ts-ignore we don't control this type triggers_actions_ui, } = plugins; - alertTypeInitializers.forEach(init => - triggers_actions_ui.alertTypeRegistry.register(init({ autocomplete })) - ); + alertTypeInitializers.forEach(init => { + const alertInitializer = init({ autocomplete }); + if (!triggers_actions_ui.alertTypeRegistry.has(alertInitializer.id)) { + triggers_actions_ui.alertTypeRegistry.register(init({ autocomplete })); + } + }); let breadcrumbs: ChromeBreadcrumb[] = []; core.chrome.getBreadcrumbs$().subscribe((nextBreadcrumbs?: ChromeBreadcrumb[]) => { @@ -68,6 +70,7 @@ export const getKibanaFrameworkAdapter = ( isLogsAvailable: logs, kibanaBreadcrumbs: breadcrumbs, plugins, + startPlugins, renderGlobalHelpControls: () => setHelpExtension({ appName: i18nFormatter.translate('xpack.uptime.header.appName', { diff --git a/x-pack/legacy/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts b/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts rename to x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/alert_types/index.ts b/x-pack/plugins/uptime/public/lib/alert_types/index.ts similarity index 67% rename from x-pack/legacy/plugins/uptime/public/lib/alert_types/index.ts rename to x-pack/plugins/uptime/public/lib/alert_types/index.ts index 74160577cb0b1..f7ab254ffe675 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/alert_types/index.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/index.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// TODO: after NP migration is complete we should be able to remove this lint ignore comment -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../../../plugins/triggers_actions_ui/public'; +import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { initMonitorStatusAlertType } from './monitor_status'; export type AlertTypeInitializer = (dependenies: { autocomplete: any }) => AlertTypeModel; diff --git a/x-pack/legacy/plugins/uptime/public/lib/alert_types/monitor_status.tsx b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx similarity index 88% rename from x-pack/legacy/plugins/uptime/public/lib/alert_types/monitor_status.tsx rename to x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx index 0624d20b197c0..e7695fb1cbb56 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/alert_types/monitor_status.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx @@ -8,17 +8,12 @@ import { PathReporter } from 'io-ts/lib/PathReporter'; import React from 'react'; import DateMath from '@elastic/datemath'; import { isRight } from 'fp-ts/lib/Either'; -import { - AlertTypeModel, - ValidationResult, - // TODO: this typing issue should be resolved after NP migration - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/triggers_actions_ui/public/types'; +import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { AlertTypeInitializer } from '.'; import { StatusCheckExecutorParamsType } from '../../../common/runtime_types'; import { AlertMonitorStatus } from '../../components/overview/alerts/alerts_containers'; -export const validate = (alertParams: any): ValidationResult => { +export const validate = (alertParams: any) => { const errors: Record = {}; const decoded = StatusCheckExecutorParamsType.decode(alertParams); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_kueries.test.ts.snap b/x-pack/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_kueries.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_kueries.test.ts.snap rename to x-pack/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_kueries.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_url_params.test.ts.snap b/x-pack/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_url_params.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_url_params.test.ts.snap rename to x-pack/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_url_params.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/combine_filters_and_user_search.test.ts b/x-pack/plugins/uptime/public/lib/helper/__tests__/combine_filters_and_user_search.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/combine_filters_and_user_search.test.ts rename to x-pack/plugins/uptime/public/lib/helper/__tests__/combine_filters_and_user_search.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/convert_measurements.test.ts b/x-pack/plugins/uptime/public/lib/helper/__tests__/convert_measurements.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/convert_measurements.test.ts rename to x-pack/plugins/uptime/public/lib/helper/__tests__/convert_measurements.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/series_has_down_values.test.ts b/x-pack/plugins/uptime/public/lib/helper/__tests__/series_has_down_values.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/series_has_down_values.test.ts rename to x-pack/plugins/uptime/public/lib/helper/__tests__/series_has_down_values.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/stringify_kueries.test.ts b/x-pack/plugins/uptime/public/lib/helper/__tests__/stringify_kueries.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/stringify_kueries.test.ts rename to x-pack/plugins/uptime/public/lib/helper/__tests__/stringify_kueries.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/stringify_url_params.test.ts b/x-pack/plugins/uptime/public/lib/helper/__tests__/stringify_url_params.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/stringify_url_params.test.ts rename to x-pack/plugins/uptime/public/lib/helper/__tests__/stringify_url_params.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts b/x-pack/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/is_within_current_date.test.ts b/x-pack/plugins/uptime/public/lib/helper/charts/__tests__/is_within_current_date.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/is_within_current_date.test.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/__tests__/is_within_current_date.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts b/x-pack/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_label_format.ts b/x-pack/plugins/uptime/public/lib/helper/charts/get_label_format.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_label_format.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/get_label_format.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/index.ts b/x-pack/plugins/uptime/public/lib/helper/charts/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/index.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/is_within_current_date.ts b/x-pack/plugins/uptime/public/lib/helper/charts/is_within_current_date.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/is_within_current_date.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/is_within_current_date.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/combine_filters_and_user_search.ts b/x-pack/plugins/uptime/public/lib/helper/combine_filters_and_user_search.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/combine_filters_and_user_search.ts rename to x-pack/plugins/uptime/public/lib/helper/combine_filters_and_user_search.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/convert_measurements.ts b/x-pack/plugins/uptime/public/lib/helper/convert_measurements.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/convert_measurements.ts rename to x-pack/plugins/uptime/public/lib/helper/convert_measurements.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/get_title.ts b/x-pack/plugins/uptime/public/lib/helper/get_title.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/get_title.ts rename to x-pack/plugins/uptime/public/lib/helper/get_title.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/helper_with_router.tsx b/x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/helper_with_router.tsx rename to x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/index.ts b/x-pack/plugins/uptime/public/lib/helper/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/index.ts rename to x-pack/plugins/uptime/public/lib/helper/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/build_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/build_href.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/index.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/index.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/series_has_down_values.ts b/x-pack/plugins/uptime/public/lib/helper/series_has_down_values.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/series_has_down_values.ts rename to x-pack/plugins/uptime/public/lib/helper/series_has_down_values.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/stringify_kueries.ts b/x-pack/plugins/uptime/public/lib/helper/stringify_kueries.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/stringify_kueries.ts rename to x-pack/plugins/uptime/public/lib/helper/stringify_kueries.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/stringify_url_params.ts b/x-pack/plugins/uptime/public/lib/helper/stringify_url_params.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/stringify_url_params.ts rename to x-pack/plugins/uptime/public/lib/helper/stringify_url_params.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap rename to x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_url_int.test.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_autorefresh_interval.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_url_int.test.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_autorefresh_interval.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_is_paused.test.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_is_paused.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_is_paused.test.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_is_paused.test.ts diff --git a/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_url_int.test.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_url_int.test.ts new file mode 100644 index 0000000000000..a5c2168378089 --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_url_int.test.ts @@ -0,0 +1,24 @@ +/* + * 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 { parseUrlInt } from '../parse_url_int'; + +describe('parseUrlInt', () => { + it('parses a number', () => { + const result = parseUrlInt('23', 50); + expect(result).toBe(23); + }); + + it('returns default value for empty string', () => { + const result = parseUrlInt('', 50); + expect(result).toBe(50); + }); + + it('returns default value for non-numeric string', () => { + const result = parseUrlInt('abc', 50); + expect(result).toBe(50); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/index.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/index.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_is_paused.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/parse_is_paused.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_is_paused.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/parse_is_paused.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts similarity index 87% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts index b1a4d0a2aba0d..0e5363527b516 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts +++ b/x-pack/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// TODO: add a comment explaining the purpose of this function export const parseUrlInt = (value: string | undefined, defaultValue: number): number => { const parsed = parseInt(value || '', 10); return isNaN(parsed) ? defaultValue : parsed; diff --git a/x-pack/legacy/plugins/uptime/public/lib/index.ts b/x-pack/plugins/uptime/public/lib/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/index.ts rename to x-pack/plugins/uptime/public/lib/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/lib.ts b/x-pack/plugins/uptime/public/lib/lib.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/lib.ts rename to x-pack/plugins/uptime/public/lib/lib.ts diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/monitor.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/monitor.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/monitor.test.tsx.snap rename to x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/monitor.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/not_found.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/not_found.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/not_found.test.tsx.snap rename to x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/not_found.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap rename to x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap rename to x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/monitor.test.tsx b/x-pack/plugins/uptime/public/pages/__tests__/monitor.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/monitor.test.tsx rename to x-pack/plugins/uptime/public/pages/__tests__/monitor.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/not_found.test.tsx b/x-pack/plugins/uptime/public/pages/__tests__/not_found.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/not_found.test.tsx rename to x-pack/plugins/uptime/public/pages/__tests__/not_found.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/overview.test.tsx b/x-pack/plugins/uptime/public/pages/__tests__/overview.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/overview.test.tsx rename to x-pack/plugins/uptime/public/pages/__tests__/overview.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/page_header.test.tsx b/x-pack/plugins/uptime/public/pages/__tests__/page_header.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/page_header.test.tsx rename to x-pack/plugins/uptime/public/pages/__tests__/page_header.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/pages/index.ts b/x-pack/plugins/uptime/public/pages/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/index.ts rename to x-pack/plugins/uptime/public/pages/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/plugins/uptime/public/pages/monitor.tsx similarity index 87% rename from x-pack/legacy/plugins/uptime/public/pages/monitor.tsx rename to x-pack/plugins/uptime/public/pages/monitor.tsx index 4495be9b24dc1..8a309db75acd2 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor.tsx @@ -7,14 +7,13 @@ import { EuiSpacer } from '@elastic/eui'; import React from 'react'; import { useSelector } from 'react-redux'; -import { useTrackPageview } from '../../../../../plugins/observability/public'; import { monitorStatusSelector } from '../state/selectors'; import { PageHeader } from './page_header'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; +import { useTrackPageview } from '../../../observability/public'; import { useMonitorId, useUptimeTelemetry, UptimePage } from '../hooks'; import { MonitorCharts } from '../components/monitor'; -import { MonitorStatusDetails } from '../components/monitor'; -import { PingList } from '../components/monitor'; +import { MonitorStatusDetails, PingList } from '../components/monitor'; export const MonitorPage: React.FC = () => { const monitorId = useMonitorId(); diff --git a/x-pack/legacy/plugins/uptime/public/pages/not_found.tsx b/x-pack/plugins/uptime/public/pages/not_found.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/not_found.tsx rename to x-pack/plugins/uptime/public/pages/not_found.tsx diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/plugins/uptime/public/pages/overview.tsx similarity index 96% rename from x-pack/legacy/plugins/uptime/public/pages/overview.tsx rename to x-pack/plugins/uptime/public/pages/overview.tsx index adc36efa6f7db..fefd804cbfabf 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/plugins/uptime/public/pages/overview.tsx @@ -10,11 +10,11 @@ import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { useUptimeTelemetry, UptimePage, useGetUrlParams } from '../hooks'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; -import { useTrackPageview } from '../../../../../plugins/observability/public'; -import { DataPublicPluginSetup, IIndexPattern } from '../../../../../../src/plugins/data/public'; -import { useUpdateKueryString } from '../hooks'; import { PageHeader } from './page_header'; +import { DataPublicPluginSetup, IIndexPattern } from '../../../../../src/plugins/data/public'; +import { useUpdateKueryString } from '../hooks'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; +import { useTrackPageview } from '../../../observability/public'; import { MonitorList } from '../components/overview/monitor_list/monitor_list_container'; import { EmptyState, FilterGroup, KueryBar, ParsingErrorCallout } from '../components/overview'; import { StatusPanel } from '../components/overview/status_panel'; diff --git a/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx b/x-pack/plugins/uptime/public/pages/page_header.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/page_header.tsx rename to x-pack/plugins/uptime/public/pages/page_header.tsx diff --git a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx b/x-pack/plugins/uptime/public/pages/settings.tsx similarity index 98% rename from x-pack/legacy/plugins/uptime/public/pages/settings.tsx rename to x-pack/plugins/uptime/public/pages/settings.tsx index d8c2a78092854..52096a49435d7 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx +++ b/x-pack/plugins/uptime/public/pages/settings.tsx @@ -24,7 +24,7 @@ import { getDynamicSettings, setDynamicSettings } from '../state/actions/dynamic import { DynamicSettings } from '../../common/runtime_types'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { OVERVIEW_ROUTE } from '../../common/constants'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { UptimePage, useUptimeTelemetry } from '../hooks'; import { IndicesForm } from '../components/settings/indices_form'; import { diff --git a/x-pack/legacy/plugins/uptime/public/pages/translations.ts b/x-pack/plugins/uptime/public/pages/translations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/translations.ts rename to x-pack/plugins/uptime/public/pages/translations.ts diff --git a/x-pack/legacy/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx similarity index 93% rename from x-pack/legacy/plugins/uptime/public/routes.tsx rename to x-pack/plugins/uptime/public/routes.tsx index b5e20ef8a70a9..eb0587c0417a2 100644 --- a/x-pack/legacy/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -6,7 +6,7 @@ import React, { FC } from 'react'; import { Route, Switch } from 'react-router-dom'; -import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; +import { DataPublicPluginSetup } from '../../../../src/plugins/data/public'; import { OverviewPage } from './components/overview/overview_container'; import { MONITOR_ROUTE, OVERVIEW_ROUTE, SETTINGS_ROUTE } from '../common/constants'; import { MonitorPage, NotFoundPage, SettingsPage } from './pages'; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/__tests__/__snapshots__/overview_filters.test.ts.snap b/x-pack/plugins/uptime/public/state/actions/__tests__/__snapshots__/overview_filters.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/__tests__/__snapshots__/overview_filters.test.ts.snap rename to x-pack/plugins/uptime/public/state/actions/__tests__/__snapshots__/overview_filters.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/__tests__/overview_filters.test.ts b/x-pack/plugins/uptime/public/state/actions/__tests__/overview_filters.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/__tests__/overview_filters.test.ts rename to x-pack/plugins/uptime/public/state/actions/__tests__/overview_filters.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/dynamic_settings.ts b/x-pack/plugins/uptime/public/state/actions/dynamic_settings.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/dynamic_settings.ts rename to x-pack/plugins/uptime/public/state/actions/dynamic_settings.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/index.ts b/x-pack/plugins/uptime/public/state/actions/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/index.ts rename to x-pack/plugins/uptime/public/state/actions/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/index_patternts.ts b/x-pack/plugins/uptime/public/state/actions/index_patternts.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/index_patternts.ts rename to x-pack/plugins/uptime/public/state/actions/index_patternts.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/index_status.ts b/x-pack/plugins/uptime/public/state/actions/index_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/index_status.ts rename to x-pack/plugins/uptime/public/state/actions/index_status.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/actions/ml_anomaly.ts similarity index 82% rename from x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts rename to x-pack/plugins/uptime/public/state/actions/ml_anomaly.ts index 67c75314a7305..441a3cefdf204 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts +++ b/x-pack/plugins/uptime/public/state/actions/ml_anomaly.ts @@ -6,15 +6,15 @@ import { createAction } from 'redux-actions'; import { createAsyncAction } from './utils'; -import { MlCapabilitiesResponse } from '../../../../../../plugins/ml/common/types/capabilities'; -import { AnomaliesTableRecord } from '../../../../../../plugins/ml/common/types/anomalies'; +import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; +import { AnomaliesTableRecord } from '../../../../../plugins/ml/common/types/anomalies'; import { CreateMLJobSuccess, DeleteJobResults, MonitorIdParam, HeartbeatIndicesParam, } from './types'; -import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer'; +import { JobExistResult } from '../../../../../plugins/ml/common/types/data_recognizer'; export const resetMLState = createAction('RESET_ML_STATE'); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts b/x-pack/plugins/uptime/public/state/actions/monitor.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts rename to x-pack/plugins/uptime/public/state/actions/monitor.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_duration.ts b/x-pack/plugins/uptime/public/state/actions/monitor_duration.ts similarity index 90% rename from x-pack/legacy/plugins/uptime/public/state/actions/monitor_duration.ts rename to x-pack/plugins/uptime/public/state/actions/monitor_duration.ts index 9a2db5be60b12..524044f873687 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_duration.ts +++ b/x-pack/plugins/uptime/public/state/actions/monitor_duration.ts @@ -7,7 +7,7 @@ import { createAction } from 'redux-actions'; import { QueryParams } from './types'; import { MonitorDurationResult } from '../../../common/types'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; type MonitorQueryParams = QueryParams & { monitorId: string }; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_list.ts b/x-pack/plugins/uptime/public/state/actions/monitor_list.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/monitor_list.ts rename to x-pack/plugins/uptime/public/state/actions/monitor_list.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts b/x-pack/plugins/uptime/public/state/actions/monitor_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts rename to x-pack/plugins/uptime/public/state/actions/monitor_status.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/overview_filters.ts b/x-pack/plugins/uptime/public/state/actions/overview_filters.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/overview_filters.ts rename to x-pack/plugins/uptime/public/state/actions/overview_filters.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ping.ts b/x-pack/plugins/uptime/public/state/actions/ping.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/ping.ts rename to x-pack/plugins/uptime/public/state/actions/ping.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts b/x-pack/plugins/uptime/public/state/actions/snapshot.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts rename to x-pack/plugins/uptime/public/state/actions/snapshot.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/types.ts b/x-pack/plugins/uptime/public/state/actions/types.ts similarity index 94% rename from x-pack/legacy/plugins/uptime/public/state/actions/types.ts rename to x-pack/plugins/uptime/public/state/actions/types.ts index 41381afd31453..dee2df77707d2 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/types.ts +++ b/x-pack/plugins/uptime/public/state/actions/types.ts @@ -5,7 +5,7 @@ */ import { Action } from 'redux-actions'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; export interface AsyncAction { get: (payload: Payload) => Action; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts b/x-pack/plugins/uptime/public/state/actions/ui.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/ui.ts rename to x-pack/plugins/uptime/public/state/actions/ui.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/utils.ts b/x-pack/plugins/uptime/public/state/actions/utils.ts similarity index 90% rename from x-pack/legacy/plugins/uptime/public/state/actions/utils.ts rename to x-pack/plugins/uptime/public/state/actions/utils.ts index a5adb96731f33..8ce4cf011406b 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/utils.ts +++ b/x-pack/plugins/uptime/public/state/actions/utils.ts @@ -6,7 +6,7 @@ import { createAction } from 'redux-actions'; import { AsyncAction, AsyncAction1 } from './types'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; export function createAsyncAction( actionStr: string diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap b/x-pack/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap rename to x-pack/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap diff --git a/x-pack/plugins/uptime/public/state/api/__tests__/ml_anomaly.test.ts b/x-pack/plugins/uptime/public/state/api/__tests__/ml_anomaly.test.ts new file mode 100644 index 0000000000000..838e5b8246b4b --- /dev/null +++ b/x-pack/plugins/uptime/public/state/api/__tests__/ml_anomaly.test.ts @@ -0,0 +1,34 @@ +/* + * 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 { getMLJobId } from '../ml_anomaly'; + +describe('ML Anomaly API', () => { + it('it generates a lowercase job id', async () => { + const monitorId = 'ABC1334haa'; + + const jobId = getMLJobId(monitorId); + + expect(jobId).toEqual(jobId.toLowerCase()); + }); + + it('should truncate long monitor IDs', () => { + const longAndWeirdMonitorId = + 'https://auto-mmmmxhhhhhccclongAndWeirdMonitorId123yyyyyrereauto-xcmpa-1345555454646'; + + expect(getMLJobId(longAndWeirdMonitorId)).toHaveLength(64); + }); + + it('should remove special characters and replace them with underscore', () => { + const monIdSpecialChars = '/ ? , " < > | * a'; + + const jobId = getMLJobId(monIdSpecialChars); + + const format = /[/?,"<>|*]+/; + + expect(format.test(jobId)).toBe(false); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts b/x-pack/plugins/uptime/public/state/api/__tests__/snapshot.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts rename to x-pack/plugins/uptime/public/state/api/__tests__/snapshot.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/dynamic_settings.ts b/x-pack/plugins/uptime/public/state/api/dynamic_settings.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/dynamic_settings.ts rename to x-pack/plugins/uptime/public/state/api/dynamic_settings.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index.ts b/x-pack/plugins/uptime/public/state/api/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/index.ts rename to x-pack/plugins/uptime/public/state/api/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index_pattern.ts b/x-pack/plugins/uptime/public/state/api/index_pattern.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/index_pattern.ts rename to x-pack/plugins/uptime/public/state/api/index_pattern.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index_status.ts b/x-pack/plugins/uptime/public/state/api/index_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/index_status.ts rename to x-pack/plugins/uptime/public/state/api/index_status.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts similarity index 71% rename from x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts rename to x-pack/plugins/uptime/public/state/api/ml_anomaly.ts index 16b90e9921428..c4ecb769abefc 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts +++ b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts @@ -8,17 +8,35 @@ import moment from 'moment'; import { apiService } from './utils'; import { AnomalyRecords, AnomalyRecordsParams } from '../actions'; import { API_URLS, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants'; -import { MlCapabilitiesResponse } from '../../../../../../plugins/ml/common/types/capabilities'; +import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; import { CreateMLJobSuccess, DeleteJobResults, MonitorIdParam, HeartbeatIndicesParam, } from '../actions/types'; -import { DataRecognizerConfigResponse } from '../../../../../../plugins/ml/common/types/modules'; -import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer'; +import { DataRecognizerConfigResponse } from '../../../../../plugins/ml/common/types/modules'; +import { JobExistResult } from '../../../../../plugins/ml/common/types/data_recognizer'; -export const getMLJobId = (monitorId: string) => `${monitorId}_${ML_JOB_ID}`.toLowerCase(); +const getJobPrefix = (monitorId: string) => { + // ML App doesn't support upper case characters in job name + // Also Spaces and the characters / ? , " < > | * are not allowed + // so we will replace all special chars with _ + + const prefix = monitorId.replace(/[^A-Z0-9]+/gi, '_').toLowerCase(); + + // ML Job ID can't be greater than 64 length, so will be substring it, and hope + // At such big length, there is minimum chance of having duplicate monitor id + // Subtracting ML_JOB_ID constant as well + const postfix = '_' + ML_JOB_ID; + + if ((prefix + postfix).length > 64) { + return prefix.substring(0, 64 - postfix.length) + '_'; + } + return prefix + '_'; +}; + +export const getMLJobId = (monitorId: string) => `${getJobPrefix(monitorId)}${ML_JOB_ID}`; export const getMLCapabilities = async (): Promise => { return await apiService.get(API_URLS.ML_CAPABILITIES); @@ -34,11 +52,8 @@ export const createMLJob = async ({ }: MonitorIdParam & HeartbeatIndicesParam): Promise => { const url = API_URLS.ML_SETUP_MODULE + ML_MODULE_ID; - // ML App doesn't support upper case characters in job name - const lowerCaseMonitorId = monitorId.toLowerCase(); - const data = { - prefix: `${lowerCaseMonitorId}_`, + prefix: `${getJobPrefix(monitorId)}`, useDedicatedIndex: false, startDatafeed: true, start: moment() @@ -48,7 +63,7 @@ export const createMLJob = async ({ query: { bool: { filter: [ - { term: { 'monitor.id': lowerCaseMonitorId } }, + { term: { 'monitor.id': monitorId } }, { range: { 'monitor.duration.us': { gt: 0 } } }, ], }, diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts b/x-pack/plugins/uptime/public/state/api/monitor.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/monitor.ts rename to x-pack/plugins/uptime/public/state/api/monitor.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor_duration.ts b/x-pack/plugins/uptime/public/state/api/monitor_duration.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/monitor_duration.ts rename to x-pack/plugins/uptime/public/state/api/monitor_duration.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor_list.ts b/x-pack/plugins/uptime/public/state/api/monitor_list.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/monitor_list.ts rename to x-pack/plugins/uptime/public/state/api/monitor_list.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts b/x-pack/plugins/uptime/public/state/api/monitor_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts rename to x-pack/plugins/uptime/public/state/api/monitor_status.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts b/x-pack/plugins/uptime/public/state/api/overview_filters.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts rename to x-pack/plugins/uptime/public/state/api/overview_filters.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ping.ts b/x-pack/plugins/uptime/public/state/api/ping.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/ping.ts rename to x-pack/plugins/uptime/public/state/api/ping.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts b/x-pack/plugins/uptime/public/state/api/snapshot.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts rename to x-pack/plugins/uptime/public/state/api/snapshot.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/types.ts b/x-pack/plugins/uptime/public/state/api/types.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/types.ts rename to x-pack/plugins/uptime/public/state/api/types.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/utils.ts b/x-pack/plugins/uptime/public/state/api/utils.ts similarity index 95% rename from x-pack/legacy/plugins/uptime/public/state/api/utils.ts rename to x-pack/plugins/uptime/public/state/api/utils.ts index e67efa8570c11..acd9bec5a74bc 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/utils.ts +++ b/x-pack/plugins/uptime/public/state/api/utils.ts @@ -6,7 +6,7 @@ import { PathReporter } from 'io-ts/lib/PathReporter'; import { isRight } from 'fp-ts/lib/Either'; -import { HttpFetchQuery, HttpSetup } from '../../../../../../../target/types/core/public'; +import { HttpFetchQuery, HttpSetup } from '../../../../../../target/types/core/public'; class ApiService { private static instance: ApiService; diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts b/x-pack/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts rename to x-pack/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/dynamic_settings.ts b/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/dynamic_settings.ts rename to x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts b/x-pack/plugins/uptime/public/state/effects/fetch_effect.ts similarity index 94% rename from x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts rename to x-pack/plugins/uptime/public/state/effects/fetch_effect.ts index b0734cb5ccabb..0aa85609fe4f0 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts +++ b/x-pack/plugins/uptime/public/state/effects/fetch_effect.ts @@ -6,7 +6,7 @@ import { call, put } from 'redux-saga/effects'; import { Action } from 'redux-actions'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; /** * Factory function for a fetch effect. It expects three action creators, diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts b/x-pack/plugins/uptime/public/state/effects/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/index.ts rename to x-pack/plugins/uptime/public/state/effects/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/index_pattern.ts b/x-pack/plugins/uptime/public/state/effects/index_pattern.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/index_pattern.ts rename to x-pack/plugins/uptime/public/state/effects/index_pattern.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/index_status.ts b/x-pack/plugins/uptime/public/state/effects/index_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/index_status.ts rename to x-pack/plugins/uptime/public/state/effects/index_status.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/effects/ml_anomaly.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/ml_anomaly.ts rename to x-pack/plugins/uptime/public/state/effects/ml_anomaly.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts b/x-pack/plugins/uptime/public/state/effects/monitor.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts rename to x-pack/plugins/uptime/public/state/effects/monitor.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor_duration.ts b/x-pack/plugins/uptime/public/state/effects/monitor_duration.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/monitor_duration.ts rename to x-pack/plugins/uptime/public/state/effects/monitor_duration.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor_list.ts b/x-pack/plugins/uptime/public/state/effects/monitor_list.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/monitor_list.ts rename to x-pack/plugins/uptime/public/state/effects/monitor_list.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor_status.ts b/x-pack/plugins/uptime/public/state/effects/monitor_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/monitor_status.ts rename to x-pack/plugins/uptime/public/state/effects/monitor_status.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/overview_filters.ts b/x-pack/plugins/uptime/public/state/effects/overview_filters.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/overview_filters.ts rename to x-pack/plugins/uptime/public/state/effects/overview_filters.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/ping.ts b/x-pack/plugins/uptime/public/state/effects/ping.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/ping.ts rename to x-pack/plugins/uptime/public/state/effects/ping.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts b/x-pack/plugins/uptime/public/state/effects/snapshot.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts rename to x-pack/plugins/uptime/public/state/effects/snapshot.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/index.ts b/x-pack/plugins/uptime/public/state/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/index.ts rename to x-pack/plugins/uptime/public/state/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/kibana_service.ts b/x-pack/plugins/uptime/public/state/kibana_service.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/kibana_service.ts rename to x-pack/plugins/uptime/public/state/kibana_service.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap b/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap rename to x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap b/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap rename to x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts b/x-pack/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts rename to x-pack/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts b/x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts rename to x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts b/x-pack/plugins/uptime/public/state/reducers/dynamic_settings.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts rename to x-pack/plugins/uptime/public/state/reducers/dynamic_settings.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts b/x-pack/plugins/uptime/public/state/reducers/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/index.ts rename to x-pack/plugins/uptime/public/state/reducers/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index_pattern.ts b/x-pack/plugins/uptime/public/state/reducers/index_pattern.ts similarity index 92% rename from x-pack/legacy/plugins/uptime/public/state/reducers/index_pattern.ts rename to x-pack/plugins/uptime/public/state/reducers/index_pattern.ts index bc482e2f35c45..b357f5a904ea6 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/index_pattern.ts +++ b/x-pack/plugins/uptime/public/state/reducers/index_pattern.ts @@ -5,7 +5,7 @@ */ import { handleActions, Action } from 'redux-actions'; import { getIndexPattern, getIndexPatternSuccess, getIndexPatternFail } from '../actions'; -import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; +import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; export interface IndexPatternState { index_pattern: IIndexPattern | null; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index_status.ts b/x-pack/plugins/uptime/public/state/reducers/index_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/index_status.ts rename to x-pack/plugins/uptime/public/state/reducers/index_status.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/reducers/ml_anomaly.ts similarity index 88% rename from x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts rename to x-pack/plugins/uptime/public/state/reducers/ml_anomaly.ts index df5e825c3488b..61e03a9592921 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts +++ b/x-pack/plugins/uptime/public/state/reducers/ml_anomaly.ts @@ -15,11 +15,11 @@ import { getMLCapabilitiesAction, } from '../actions'; import { getAsyncInitialState, handleAsyncAction } from './utils'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; import { AsyncInitialState } from './types'; -import { MlCapabilitiesResponse } from '../../../../../../plugins/ml/common/types/capabilities'; +import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; import { CreateMLJobSuccess, DeleteJobResults } from '../actions/types'; -import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer'; +import { JobExistResult } from '../../../../../plugins/ml/common/types/data_recognizer'; export interface MLJobState { mlJob: AsyncInitialState; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts b/x-pack/plugins/uptime/public/state/reducers/monitor.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts rename to x-pack/plugins/uptime/public/state/reducers/monitor.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_duration.ts b/x-pack/plugins/uptime/public/state/reducers/monitor_duration.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/monitor_duration.ts rename to x-pack/plugins/uptime/public/state/reducers/monitor_duration.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_list.ts b/x-pack/plugins/uptime/public/state/reducers/monitor_list.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/monitor_list.ts rename to x-pack/plugins/uptime/public/state/reducers/monitor_list.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts b/x-pack/plugins/uptime/public/state/reducers/monitor_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts rename to x-pack/plugins/uptime/public/state/reducers/monitor_status.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/overview_filters.ts b/x-pack/plugins/uptime/public/state/reducers/overview_filters.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/overview_filters.ts rename to x-pack/plugins/uptime/public/state/reducers/overview_filters.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ping.ts b/x-pack/plugins/uptime/public/state/reducers/ping.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/ping.ts rename to x-pack/plugins/uptime/public/state/reducers/ping.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ping_list.ts b/x-pack/plugins/uptime/public/state/reducers/ping_list.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/ping_list.ts rename to x-pack/plugins/uptime/public/state/reducers/ping_list.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts b/x-pack/plugins/uptime/public/state/reducers/snapshot.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts rename to x-pack/plugins/uptime/public/state/reducers/snapshot.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/types.ts b/x-pack/plugins/uptime/public/state/reducers/types.ts similarity index 81% rename from x-pack/legacy/plugins/uptime/public/state/reducers/types.ts rename to x-pack/plugins/uptime/public/state/reducers/types.ts index 88995a2f5dd70..c81ee6875f305 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/types.ts +++ b/x-pack/plugins/uptime/public/state/reducers/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; export interface AsyncInitialState { data: ReduceStateType | null; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts b/x-pack/plugins/uptime/public/state/reducers/ui.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts rename to x-pack/plugins/uptime/public/state/reducers/ui.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/utils.ts b/x-pack/plugins/uptime/public/state/reducers/utils.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/utils.ts rename to x-pack/plugins/uptime/public/state/reducers/utils.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts rename to x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts b/x-pack/plugins/uptime/public/state/selectors/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/selectors/index.ts rename to x-pack/plugins/uptime/public/state/selectors/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/plugins/uptime/public/uptime_app.tsx similarity index 75% rename from x-pack/legacy/plugins/uptime/public/uptime_app.tsx rename to x-pack/plugins/uptime/public/uptime_app.tsx index 92775a2663863..0d18f959230d1 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/plugins/uptime/public/uptime_app.tsx @@ -10,13 +10,14 @@ import React, { useEffect } from 'react'; import { Provider as ReduxProvider } from 'react-redux'; import { BrowserRouter as Router } from 'react-router-dom'; import { I18nStart, ChromeBreadcrumb, CoreStart } from 'src/core/public'; -import { PluginsSetup } from 'ui/new_platform/new_platform'; -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; +import { ClientPluginsSetup, ClientPluginsStart } from './apps/plugin'; import { UMUpdateBadge } from './lib/lib'; import { UptimeRefreshContextProvider, UptimeSettingsContextProvider, UptimeThemeContextProvider, + UptimeStartupPluginsContextProvider, } from './contexts'; import { CommonlyUsedRange } from './components/common/uptime_date_picker'; import { store } from './state'; @@ -47,7 +48,8 @@ export interface UptimeAppProps { isInfraAvailable: boolean; isLogsAvailable: boolean; kibanaBreadcrumbs: ChromeBreadcrumb[]; - plugins: PluginsSetup; + plugins: ClientPluginsSetup; + startPlugins: ClientPluginsStart; routerBasename: string; setBadge: UMUpdateBadge; renderGlobalHelpControls(): void; @@ -66,6 +68,7 @@ const Application = (props: UptimeAppProps) => { renderGlobalHelpControls, routerBasename, setBadge, + startPlugins, } = props; useEffect(() => { @@ -87,7 +90,6 @@ const Application = (props: UptimeAppProps) => { kibanaService.core = core; - // @ts-ignore store.dispatch(setBasePath(basePath)); return ( @@ -99,17 +101,19 @@ const Application = (props: UptimeAppProps) => { - - -
- - -
-
-
+ + + +
+ + +
+
+
+
diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index 725b53aeca02d..d68bbabe82b86 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -5,7 +5,7 @@ */ import { Request, Server } from 'hapi'; -import { PLUGIN } from '../../../legacy/plugins/uptime/common/constants'; +import { PLUGIN } from '../common/constants'; import { compose } from './lib/compose/kibana'; import { initUptimeServer } from './uptime_server'; import { UptimeCorePlugins, UptimeCoreSetup } from './lib/adapters/framework'; diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index 98c6be5aa3c8e..f4d1c72770494 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -13,7 +13,7 @@ import { } from 'src/core/server'; import { UMKibanaRoute } from '../../../rest_api'; import { PluginSetupContract } from '../../../../../features/server'; -import { DynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DynamicSettings } from '../../../../common/runtime_types'; export type APICaller = ( endpoint: string, @@ -31,7 +31,7 @@ export type UMSavedObjectsQueryFn = ( ) => Promise | T; export interface UptimeCoreSetup { - route: IRouter; + router: IRouter; } export interface UptimeCorePlugins { diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts index 0176471aec1be..46f46720d4c04 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -21,10 +21,10 @@ export class UMKibanaBackendFrameworkAdapter implements UMBackendFrameworkAdapte }; switch (method) { case 'GET': - this.server.route.get(routeDefinition, handler); + this.server.router.get(routeDefinition, handler); break; case 'POST': - this.server.route.post(routeDefinition, handler); + this.server.router.post(routeDefinition, handler); break; default: throw new Error(`Handler for method ${method} is not defined`); diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index 4f4c6e3011ad1..24da3f3fa4d06 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -16,7 +16,7 @@ import { AlertType } from '../../../../../alerting/server'; import { IRouter } from 'kibana/server'; import { UMServerLibs } from '../../lib'; import { UptimeCoreSetup } from '../../adapters'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; /** @@ -27,10 +27,10 @@ import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mo * so we don't have to mock them all for each test. */ const bootstrapDependencies = (customRequests?: any) => { - const route: IRouter = {} as IRouter; + const router: IRouter = {} as IRouter; // these server/libs parameters don't have any functionality, which is fine // because we aren't testing them here - const server: UptimeCoreSetup = { route }; + const server: UptimeCoreSetup = { router }; const libs: UMServerLibs = { requests: {} } as UMServerLibs; libs.requests = { ...libs.requests, ...customRequests }; return { server, libs }; diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 829e6f92d3702..f9df559a3977b 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -9,14 +9,14 @@ import { isRight } from 'fp-ts/lib/Either'; import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; import { i18n } from '@kbn/i18n'; import { AlertExecutorOptions } from '../../../../alerting/server'; -import { ACTION_GROUP_DEFINITIONS } from '../../../../../legacy/plugins/uptime/common/constants'; import { UptimeAlertTypeFactory } from './types'; import { GetMonitorStatusResult } from '../requests'; import { StatusCheckExecutorParamsType, StatusCheckAlertStateType, StatusCheckAlertState, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +} from '../../../common/runtime_types'; +import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants'; import { savedObjectsAdapter } from '../saved_objects'; const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; diff --git a/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts b/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts index fb44f5727aab3..26515fb4b4c63 100644 --- a/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts +++ b/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts @@ -5,7 +5,7 @@ */ import DateMath from '@elastic/datemath'; -import { QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; +import { QUERY } from '../../../common/constants'; export const parseRelativeDate = (dateStr: string, options = {}) => { // We need this this parsing because if user selects This week or this date diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts index b49a6b22ff976..894e2316dc927 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts @@ -5,7 +5,7 @@ */ import { getCerts } from '../get_certs'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('getCerts', () => { let mockHits: any; diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts index 03e2bc7a44bd0..75bf5096bd997 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts @@ -5,7 +5,7 @@ */ import { getLatestMonitor } from '../get_latest_monitor'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('getLatestMonitor', () => { let expectedGetLatestSearchParams: any; diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts index 5d3f9ce8d4ad9..45be1df3e8d3b 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts @@ -7,7 +7,7 @@ import { set } from 'lodash'; import mockChartsData from './monitor_charts_mock.json'; import { getMonitorDurationChart } from '../get_monitor_duration'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('ElasticsearchMonitorsAdapter', () => { it('getMonitorChartsData will provide expected filters', async () => { diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts index e47be617d7c99..82e624221c301 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts @@ -6,8 +6,8 @@ import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; import { getMonitorStatus } from '../get_monitor_status'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; import { ScopedClusterClient } from 'src/core/server'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; interface BucketItemCriteria { monitor_id: string; diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts index 4de7d3ffd2a7d..e456670a5e68d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts @@ -5,7 +5,7 @@ */ import { getPingHistogram } from '../get_ping_histogram'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('getPingHistogram', () => { const standardMockResponse: any = { diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts index abd3655cc6402..fd890a30cf742 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts @@ -6,7 +6,7 @@ import { getPings } from '../get_pings'; import { set } from 'lodash'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('getAll', () => { let mockEsSearchResult: any; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts index 4f99fbe94d54c..b427e7cae1a7e 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts @@ -5,7 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Cert, GetCertsParams } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { Cert, GetCertsParams } from '../../../common/runtime_types'; export const getCerts: UMElasticsearchQueryFn = async ({ callES, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts b/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts index 95d23ddcbf466..dbe71cf689214 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts @@ -5,7 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { OverviewFilters } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { OverviewFilters } from '../../../common/runtime_types'; import { generateFilterAggs } from './generate_filter_aggs'; export interface GetFilterBarParams { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts index 6f7854d35b308..7688f04f1acd9 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts @@ -5,7 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { StatesIndexStatus } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { StatesIndexStatus } from '../../../common/runtime_types'; export const getIndexStatus: UMElasticsearchQueryFn<{}, StatesIndexStatus> = async ({ callES, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts index a8e9ccb875a08..98ce449002f21 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts @@ -5,7 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Ping } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { Ping } from '../../../common/runtime_types'; export interface GetLatestMonitorParams { /** @member dateRangeStart timestamp bounds */ diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts index 4ce7176b57b19..cf4ffa339ddfc 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts @@ -5,10 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { - MonitorDetails, - MonitorError, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { MonitorDetails, MonitorError } from '../../../common/runtime_types'; export interface GetMonitorDetailsParams { monitorId: string; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts index e9c745b0a8713..ea2a7e790652b 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts @@ -5,11 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; -import { - LocationDurationLine, - MonitorDurationResult, -} from '../../../../../legacy/plugins/uptime/common/types'; +import { LocationDurationLine, MonitorDurationResult } from '../../../common/types'; +import { QUERY } from '../../../common/constants'; export interface GetMonitorChartsParams { /** @member monitorId ID value for the selected monitor */ diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts index f49e404ffb084..c8d3ca043edc5 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts @@ -5,11 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { UNNAMED_LOCATION } from '../../../../../legacy/plugins/uptime/common/constants'; -import { - MonitorLocations, - MonitorLocation, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { MonitorLocations, MonitorLocation } from '../../../common/runtime_types'; +import { UNNAMED_LOCATION } from '../../../common/constants'; /** * Fetch data for the monitor page title. diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts index 4b40943a85705..b1791dd04861c 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts @@ -4,15 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { CONTEXT_DEFAULTS } from '../../../common/constants'; import { fetchPage } from './search'; import { UMElasticsearchQueryFn } from '../adapters'; -import { - SortOrder, - CursorDirection, - MonitorSummary, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; - +import { MonitorSummary, SortOrder, CursorDirection } from '../../../common/runtime_types'; import { QueryContext } from './search'; export interface CursorPagination { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts index 5a8927764ea5c..299913c8dff08 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts @@ -5,12 +5,9 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; import { getFilterClause } from '../helper'; -import { - HistogramResult, - HistogramQueryResult, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { HistogramResult, HistogramQueryResult } from '../../../common/runtime_types'; +import { QUERY } from '../../../common/constants'; export interface GetPingHistogramParams { /** @member dateRangeStart timestamp bounds */ diff --git a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts index 6eccfdb13cef7..a6a0e3c3d6542 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts @@ -10,7 +10,7 @@ import { HttpResponseBody, PingsResponse, Ping, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +} from '../../../common/runtime_types'; const DEFAULT_PAGE_SIZE = 25; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts index 01f2ad88161cf..b57bc87d45418 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts @@ -5,8 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Snapshot } from '../../../../../legacy/plugins/uptime/common/runtime_types'; -import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { CONTEXT_DEFAULTS } from '../../../common/constants'; +import { Snapshot } from '../../../common/runtime_types'; import { QueryContext } from './search'; export interface GetSnapshotCountParams { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts index 2a8f681ab3453..d4ad80c85ec3d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts @@ -12,7 +12,7 @@ import { MonitorGroupsPage, } from '../fetch_page'; import { QueryContext } from '../query_context'; -import { MonitorSummary } from '../../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { MonitorSummary } from '../../../../../common/runtime_types'; import { nextPagination, prevPagination, simpleQueryContext } from './test_helpers'; const simpleFixture: MonitorGroups[] = [ diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts index 84774cdeed856..e53fff429dd8d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts @@ -6,10 +6,7 @@ import { QueryContext } from '../query_context'; import { CursorPagination } from '../types'; -import { - CursorDirection, - SortOrder, -} from '../../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection, SortOrder } from '../../../../../common/runtime_types'; describe(QueryContext, () => { // 10 minute range diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts index 47034c2130116..40775bde1c7f5 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts @@ -5,10 +5,7 @@ */ import { CursorPagination } from '../types'; -import { - CursorDirection, - SortOrder, -} from '../../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection, SortOrder } from '../../../../../common/runtime_types'; import { QueryContext } from '../query_context'; export const prevPagination = (key: any): CursorPagination => { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts index 4739c804d24e7..8612d71dfe939 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts @@ -6,14 +6,14 @@ import { get, sortBy } from 'lodash'; import { QueryContext } from './query_context'; -import { QUERY, STATES } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { QUERY } from '../../../../common/constants'; import { Check, Histogram, MonitorSummary, CursorDirection, SortOrder, -} from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +} from '../../../../common/runtime_types'; import { MonitorEnricher } from './fetch_page'; export const enrichMonitorGroups: MonitorEnricher = async ( @@ -314,7 +314,7 @@ const getHistogramForMonitors = async ( by_id: { terms: { field: 'monitor.id', - size: STATES.LEGACY_STATES_QUERY_SIZE, + size: queryContext.size, }, aggs: { histogram: { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts b/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts index 84167840d5d9b..bef8106ad1896 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts @@ -7,12 +7,8 @@ import { flatten } from 'lodash'; import { CursorPagination } from './types'; import { QueryContext } from './query_context'; -import { QUERY } from '../../../../../../legacy/plugins/uptime/common/constants'; -import { - CursorDirection, - MonitorSummary, - SortOrder, -} from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { QUERY } from '../../../../common/constants'; +import { CursorDirection, MonitorSummary, SortOrder } from '../../../../common/runtime_types'; import { enrichMonitorGroups } from './enrich_monitor_groups'; import { MonitorGroupIterator } from './monitor_group_iterator'; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts index 3449febfa5b05..e60c52660915a 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts @@ -5,7 +5,7 @@ */ import { get, set } from 'lodash'; -import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection } from '../../../../common/runtime_types'; import { QueryContext } from './query_context'; // This is the first phase of the query. In it, we find the most recent check groups that matched the given query. diff --git a/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts b/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts index 31d9166eb1e73..2fb9562028258 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts @@ -6,7 +6,7 @@ import { QueryContext } from './query_context'; import { fetchChunk } from './fetch_chunk'; -import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection } from '../../../../common/runtime_types'; import { MonitorGroups } from './fetch_page'; import { CursorPagination } from './types'; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts index 43fc54fb25808..977c32ad1f984 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts @@ -5,7 +5,7 @@ */ import { QueryContext } from './query_context'; -import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection } from '../../../../common/runtime_types'; import { MonitorGroups, MonitorLocCheckGroup } from './fetch_page'; /** diff --git a/x-pack/plugins/uptime/server/lib/requests/search/types.ts b/x-pack/plugins/uptime/server/lib/requests/search/types.ts index 2ec52d400b597..35e9647196454 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/types.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/types.ts @@ -4,10 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - CursorDirection, - SortOrder, -} from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection, SortOrder } from '../../../../common/runtime_types'; export interface CursorPagination { cursorKey?: any; diff --git a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts index 84154429b9188..69507d2950cd8 100644 --- a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts +++ b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts @@ -8,10 +8,17 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { HistogramResult, Ping, - PingsResponse as PingResults, + PingsResponse, GetCertsParams, GetPingsParams, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; + Cert, + OverviewFilters, + MonitorDetails, + MonitorLocations, + Snapshot, + StatesIndexStatus, +} from '../../../common/runtime_types'; +import { MonitorDurationResult } from '../../../common/types'; import { GetFilterBarParams, GetLatestMonitorParams, @@ -23,17 +30,8 @@ import { GetMonitorStatusParams, GetMonitorStatusResult, } from '.'; -import { - OverviewFilters, - MonitorDetails, - MonitorLocations, - Snapshot, - StatesIndexStatus, - Cert, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; import { GetMonitorStatesResult } from './get_monitor_states'; import { GetSnapshotCountParams } from './get_snapshot_counts'; -import { MonitorDurationResult } from '../../../../../legacy/plugins/uptime/common/types'; type ESQ = UMElasticsearchQueryFn; @@ -47,7 +45,7 @@ export interface UptimeRequests { getMonitorLocations: ESQ; getMonitorStates: ESQ; getMonitorStatus: ESQ; - getPings: ESQ; + getPings: ESQ; getPingHistogram: ESQ; getSnapshotCount: ESQ; getIndexStatus: ESQ<{}, StatesIndexStatus>; diff --git a/x-pack/plugins/uptime/server/lib/saved_objects.ts b/x-pack/plugins/uptime/server/lib/saved_objects.ts index d849fbd8ce0a8..28b9eaad2cf6f 100644 --- a/x-pack/plugins/uptime/server/lib/saved_objects.ts +++ b/x-pack/plugins/uptime/server/lib/saved_objects.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DynamicSettings } from '../../../../legacy/plugins/uptime/common/runtime_types'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../legacy/plugins/uptime/common/constants'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../common/constants'; +import { DynamicSettings } from '../../common/runtime_types'; import { SavedObjectsType, SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import { UMSavedObjectsQueryFn } from './adapters'; diff --git a/x-pack/plugins/uptime/server/plugin.ts b/x-pack/plugins/uptime/server/plugin.ts index 7cc591a6b2db1..13d1ae216f204 100644 --- a/x-pack/plugins/uptime/server/plugin.ts +++ b/x-pack/plugins/uptime/server/plugin.ts @@ -8,19 +8,20 @@ import { PluginInitializerContext, CoreStart, CoreSetup, + Plugin as PluginType, ISavedObjectsRepository, } from '../../../../src/core/server'; import { initServerWithKibana } from './kibana.index'; import { KibanaTelemetryAdapter, UptimeCorePlugins } from './lib/adapters'; import { umDynamicSettings } from './lib/saved_objects'; -export class Plugin { +export class Plugin implements PluginType { private savedObjectsClient?: ISavedObjectsRepository; constructor(_initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup, plugins: UptimeCorePlugins) { - initServerWithKibana({ route: core.http.createRouter() }, plugins); + initServerWithKibana({ router: core.http.createRouter() }, plugins); core.savedObjects.registerType(umDynamicSettings); KibanaTelemetryAdapter.registerUsageCollector( plugins.usageCollection, @@ -28,7 +29,9 @@ export class Plugin { ); } - public start(_core: CoreStart, _plugins: any) { - this.savedObjectsClient = _core.savedObjects.createInternalRepository(); + public start(core: CoreStart, _plugins: any) { + this.savedObjectsClient = core.savedObjects.createInternalRepository(); } + + public stop() {} } diff --git a/x-pack/plugins/uptime/server/rest_api/certs.ts b/x-pack/plugins/uptime/server/rest_api/certs.ts index 31fb3f4ab96a7..f2e1700b23e7d 100644 --- a/x-pack/plugins/uptime/server/rest_api/certs.ts +++ b/x-pack/plugins/uptime/server/rest_api/certs.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../lib/lib'; import { UMRestApiRouteFactory } from '.'; -import { API_URLS } from '../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../common/constants'; const DEFAULT_INDEX = 0; const DEFAULT_SIZE = 25; diff --git a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts index 3f4e2fc345182..31833a25ee8ac 100644 --- a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts +++ b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts @@ -8,10 +8,7 @@ import { schema } from '@kbn/config-schema'; import { isRight } from 'fp-ts/lib/Either'; import { PathReporter } from 'io-ts/lib/PathReporter'; import { UMServerLibs } from '../lib/lib'; -import { - DynamicSettings, - DynamicSettingsType, -} from '../../../../legacy/plugins/uptime/common/runtime_types'; +import { DynamicSettings, DynamicSettingsType } from '../../common/runtime_types'; import { UMRestApiRouteFactory } from '.'; import { savedObjectsAdapter } from '../lib/saved_objects'; diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts index 689a75c5903a6..26715f0ff37b6 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts @@ -6,7 +6,7 @@ import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const createGetIndexPatternRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts index 8ed73d90b2389..9a4280efa98f9 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts @@ -6,7 +6,7 @@ import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../common/constants'; export const createGetIndexStatusRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts index 5cb4e8a6241b7..60b3eafaa765e 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts @@ -6,8 +6,7 @@ import { schema } from '@kbn/config-schema'; import { UMRestApiRouteFactory } from '../types'; -import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS, CONTEXT_DEFAULTS } from '../../../common/constants'; export const createMonitorListRoute: UMRestApiRouteFactory = libs => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts index 66ce9871506d4..a110209043a7e 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const createGetMonitorLocationsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts index 9cf1340fb9409..bb002f8a8c286 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../common/constants'; export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts index 1cc010781457e..69e719efb0719 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts index 9743ced13350a..34313211061b0 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const createGetMonitorDurationRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts index deac05f36c8dc..00cbaf0d16723 100644 --- a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts +++ b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; import { objectValuesToArrays } from '../../lib/helper'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; const arrayOrStringType = schema.maybe( schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts index dceef5ecb7848..41078f735920b 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts index 80a887a7f64a9..d97195a7fe2b1 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts @@ -9,8 +9,8 @@ import { isLeft } from 'fp-ts/lib/Either'; import { PathReporter } from 'io-ts/lib/PathReporter'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; -import { GetPingsParamsType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { API_URLS } from '../../../common/constants'; +import { GetPingsParamsType } from '../../../common/runtime_types'; export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts index d870f49280117..7809e102a499f 100644 --- a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts +++ b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../common/constants'; export const createGetSnapshotCount: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts index 4b2db71037071..d8387e79e9089 100644 --- a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts +++ b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { KibanaTelemetryAdapter } from '../../lib/adapters/telemetry'; import { UMRestApiRouteFactory } from '../types'; import { PageViewParams } from '../../lib/adapters/telemetry/types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../common/constants'; export const createLogPageViewRoute: UMRestApiRouteFactory = () => ({ method: 'POST', diff --git a/x-pack/plugins/uptime/server/rest_api/types.ts b/x-pack/plugins/uptime/server/rest_api/types.ts index e05e7a4d7faf1..8720b9dc60b12 100644 --- a/x-pack/plugins/uptime/server/rest_api/types.ts +++ b/x-pack/plugins/uptime/server/rest_api/types.ts @@ -15,8 +15,8 @@ import { KibanaRequest, KibanaResponseFactory, IKibanaResponse, -} from 'src/core/server'; -import { DynamicSettings } from '../../../../legacy/plugins/uptime/common/runtime_types'; +} from 'kibana/server'; +import { DynamicSettings } from '../../common/runtime_types'; import { UMServerLibs } from '../lib/lib'; /** diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts index a7551ad7e2fad..1244657ed9988 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // node ../scripts/functional_test_runner.js --grep "Actions.servicenddd" --config=test/alerting_api_integration/security_and_spaces/config.ts diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts index 46258e41d5d69..4151deab45213 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts index 338610e9243a4..bae6dada48fb7 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // eslint-disable-next-line import/no-default-export export default function webhookTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 457b7621e84bd..870ed3cf0cc0f 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -8,7 +8,7 @@ import path from 'path'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; -import { getAllExternalServiceSimulatorPaths } from './fixtures/plugins/actions'; +import { getAllExternalServiceSimulatorPaths } from './fixtures/plugins/actions_simulators'; interface CreateTestConfigOptions { license: string; @@ -75,7 +75,6 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) 'some.non.existent.com', ])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, - '--xpack.alerting.enabled=true', '--xpack.eventLog.logEntries=true', `--xpack.actions.preconfigured=${JSON.stringify([ { @@ -124,7 +123,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ])}`, ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, - `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`, + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions_simulators')}`, `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'task_manager')}`, `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'aad')}`, `--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts index 05139213b76b9..400aec7e11c8d 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts @@ -21,7 +21,7 @@ interface CheckAADRequest extends Hapi.Request { // eslint-disable-next-line import/no-default-export export default function(kibana: any) { return new kibana.Plugin({ - require: ['actions', 'alerting', 'encryptedSavedObjects'], + require: ['encryptedSavedObjects'], name: 'aad-fixtures', init(server: Legacy.Server) { const newPlatform = ((server as unknown) as KbnServer).newPlatform; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/README.md b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/README.md similarity index 100% rename from x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/README.md rename to x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/README.md diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/index.ts similarity index 98% rename from x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts rename to x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/index.ts index 019b15cc1862a..45edd4c092da9 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/index.ts @@ -36,7 +36,7 @@ export function getAllExternalServiceSimulatorPaths(): string[] { // eslint-disable-next-line import/no-default-export export default function(kibana: any) { return new kibana.Plugin({ - require: ['xpack_main', 'actions'], + require: ['xpack_main'], name: NAME, init: (server: Hapi.Server) => { // this action is specifically NOT enabled in ../../config.ts diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/package.json b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/package.json similarity index 100% rename from x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/package.json rename to x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/package.json diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/pagerduty_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/pagerduty_simulation.ts similarity index 100% rename from x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/pagerduty_simulation.ts rename to x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/pagerduty_simulation.ts diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/servicenow_simulation.ts similarity index 100% rename from x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts rename to x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/servicenow_simulation.ts diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/slack_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/slack_simulation.ts similarity index 100% rename from x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/slack_simulation.ts rename to x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/slack_simulation.ts diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/webhook_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/webhook_simulation.ts similarity index 100% rename from x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/webhook_simulation.ts rename to x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/webhook_simulation.ts diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts index 43d533ad3ae14..1a47addf36ab3 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts @@ -11,8 +11,8 @@ import { ActionTypeExecutorOptions, ActionType } from '../../../../../../plugins // eslint-disable-next-line import/no-default-export export default function(kibana: any) { return new kibana.Plugin({ - require: ['xpack_main', 'actions', 'alerting', 'elasticsearch'], - name: 'alerts', + require: ['xpack_main', 'elasticsearch'], + name: 'alerts-fixture', init(server: any) { const clusterClient = server.newPlatform.start.core.elasticsearch.legacy.client; server.plugins.xpack_main.registerFeature({ diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/index.ts index 29708f86b0a9b..ac32f05805e4a 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/index.ts @@ -31,7 +31,7 @@ const taskByIdQuery = (id: string) => ({ export default function(kibana: any) { return new kibana.Plugin({ name: 'taskManagerHelpers', - require: ['elasticsearch', 'task_manager'], + require: ['elasticsearch'], config(Joi: any) { return Joi.object({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index eeb0818b5fbab..4c76ebfb93b0b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // eslint-disable-next-line import/no-default-export export default function pagerdutyTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 054f8f6141817..399ae0f27f5b1 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // node ../scripts/functional_test_runner.js --grep "servicenow" --config=test/alerting_api_integration/security_and_spaces/config.ts diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index e00589b7e85b7..386254e49c19c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index fd996ea4507ba..9b66326fa6157 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; const defaultValues: Record = { headers: null, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts index 5122a74d53b72..112149a32649a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // eslint-disable-next-line import/no-default-export export default function webhookTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/endpoint/resolver.ts b/x-pack/test/api_integration/apis/endpoint/resolver.ts index e8d336e875b99..73fe435764b74 100644 --- a/x-pack/test/api_integration/apis/endpoint/resolver.ts +++ b/x-pack/test/api_integration/apis/endpoint/resolver.ts @@ -27,72 +27,65 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC it('should return details for the root node', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/${entityID}/related?legacyEndpointID=${endpointID}`) + .get(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}`) .set(commonHeaders) .expect(200); expect(body.events.length).to.eql(1); - expect(body.pagination.next).to.eql(cursor); - expect(body.pagination.total).to.eql(1); - // default limit - expect(body.pagination.limit).to.eql(100); + expect(body.pagination.nextEvent).to.eql(null); }); it('returns no values when there is no more data', async () => { const { body } = await supertest // after is set to the document id of the last event so there shouldn't be any more after it .get( - `/api/endpoint/resolver/${entityID}/related?legacyEndpointID=${endpointID}&after=${cursor}` + `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=${cursor}` ) .set(commonHeaders) .expect(200); expect(body.events).be.empty(); - expect(body.pagination.next).to.eql(null); - expect(body.pagination.total).to.eql(1); + expect(body.pagination.nextEvent).to.eql(null); }); it('should return the first page of information when the cursor is invalid', async () => { const { body } = await supertest .get( - `/api/endpoint/resolver/${entityID}/related?legacyEndpointID=${endpointID}&after=blah` + `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=blah` ) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(1); - expect(body.pagination.next).to.not.eql(null); + expect(body.pagination.nextEvent).to.eql(null); }); it('should error on invalid pagination values', async () => { await supertest - .get(`/api/endpoint/resolver/${entityID}/related?limit=0`) + .get(`/api/endpoint/resolver/${entityID}/events?events=0`) .set(commonHeaders) .expect(400); await supertest - .get(`/api/endpoint/resolver/${entityID}/related?limit=2000`) + .get(`/api/endpoint/resolver/${entityID}/events?events=2000`) .set(commonHeaders) .expect(400); await supertest - .get(`/api/endpoint/resolver/${entityID}/related?limit=-1`) + .get(`/api/endpoint/resolver/${entityID}/events?events=-1`) .set(commonHeaders) .expect(400); }); it('should not find any events', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/5555/related`) + .get(`/api/endpoint/resolver/5555/events`) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextEvent).to.eql(null); expect(body.events).to.be.empty(); }); it('should return no results for an invalid endpoint ID', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/${entityID}/related?legacyEndpointID=foo`) + .get(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=foo`) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextEvent).to.eql(null); expect(body.events).to.be.empty(); }); }); @@ -103,48 +96,46 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC it('should return details for the root node', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/${entityID}?legacyEndpointID=${endpointID}&ancestors=5`) + .get( + `/api/endpoint/resolver/${entityID}/ancestry?legacyEndpointID=${endpointID}&ancestors=5` + ) .set(commonHeaders) .expect(200); expect(body.lifecycle.length).to.eql(2); - expect(body.ancestors.length).to.eql(1); - expect(body.pagination.next).to.eql(null); - // 5 is default parameter - expect(body.pagination.ancestors).to.eql(5); + expect(body.parent).not.to.eql(null); + expect(body.pagination.nextAncestor).to.eql(null); }); it('should have a populated next parameter', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/${entityID}?legacyEndpointID=${endpointID}`) + .get(`/api/endpoint/resolver/${entityID}/ancestry?legacyEndpointID=${endpointID}`) .set(commonHeaders) .expect(200); - expect(body.pagination.next).to.eql('94041'); + expect(body.pagination.nextAncestor).to.eql('94041'); }); it('should handle an ancestors param request', async () => { let { body } = await supertest - .get(`/api/endpoint/resolver/${entityID}?legacyEndpointID=${endpointID}`) + .get(`/api/endpoint/resolver/${entityID}/ancestry?legacyEndpointID=${endpointID}`) .set(commonHeaders) .expect(200); - const next = body.pagination.next; + const next = body.pagination.nextAncestor; ({ body } = await supertest - .get(`/api/endpoint/resolver/${next}?legacyEndpointID=${endpointID}&ancestors=1`) + .get(`/api/endpoint/resolver/${next}/ancestry?legacyEndpointID=${endpointID}&ancestors=1`) .set(commonHeaders) .expect(200)); expect(body.lifecycle.length).to.eql(1); - expect(body.ancestors.length).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextAncestor).to.eql(null); }); it('should handle an invalid id', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/alskdjflasj`) + .get(`/api/endpoint/resolver/alskdjflasj/ancestry`) .set(commonHeaders) .expect(200); expect(body.lifecycle.length).to.eql(0); - expect(body.ancestors.length).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextAncestor).to.eql(null); }); }); @@ -158,51 +149,58 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC .get(`/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}`) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(1); - expect(body.pagination.next).to.eql(cursor); - // default limit - expect(body.pagination.limit).to.eql(10); - expect(body.children.length).to.eql(1); expect(body.children[0].lifecycle.length).to.eql(2); expect(body.children[0].lifecycle[0].endgame.unique_pid).to.eql(94042); }); + it('returns multiple levels of child process lifecycle events', async () => { + const { body } = await supertest + .get(`/api/endpoint/resolver/93802/children?legacyEndpointID=${endpointID}&generations=3`) + .set(commonHeaders) + .expect(200); + expect(body.pagination.nextChild).to.be(null); + expect(body.children[0].pagination.nextChild).to.be(null); + + expect(body.children.length).to.eql(8); + expect(body.children[0].children[0].lifecycle.length).to.eql(2); + expect(body.children[0].lifecycle[0].endgame.unique_pid).to.eql(93932); + }); + it('returns no values when there is no more data', async () => { const { body } = await supertest // after is set to the document id of the last event so there shouldn't be any more after it .get( - `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&after=${cursor}` + `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&afterChild=${cursor}` ) .set(commonHeaders) .expect(200); expect(body.children).be.empty(); - expect(body.pagination.next).to.eql(null); - expect(body.pagination.total).to.eql(1); + expect(body.pagination.nextChild).to.eql(null); }); it('returns the first page of information when the cursor is invalid', async () => { const { body } = await supertest .get( - `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&after=blah` + `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&afterChild=blah` ) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(1); - expect(body.pagination.next).to.not.eql(null); + expect(body.children.length).to.eql(1); + expect(body.pagination.nextChild).to.be(null); }); it('errors on invalid pagination values', async () => { await supertest - .get(`/api/endpoint/resolver/${entityID}/children?limit=0`) + .get(`/api/endpoint/resolver/${entityID}/children?children=0`) .set(commonHeaders) .expect(400); await supertest - .get(`/api/endpoint/resolver/${entityID}/children?limit=2000`) + .get(`/api/endpoint/resolver/${entityID}/children?children=2000`) .set(commonHeaders) .expect(400); await supertest - .get(`/api/endpoint/resolver/${entityID}/children?limit=-1`) + .get(`/api/endpoint/resolver/${entityID}/children?children=-1`) .set(commonHeaders) .expect(400); }); @@ -212,8 +210,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC .get(`/api/endpoint/resolver/5555/children`) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextChild).to.eql(null); expect(body.children).to.be.empty(); }); @@ -222,10 +219,26 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC .get(`/api/endpoint/resolver/${entityID}/children?legacyEndpointID=foo`) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextChild).to.eql(null); expect(body.children).to.be.empty(); }); }); + + describe('tree endpoint', () => { + const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; + + it('returns ancestors, events, children, and current process lifecycle', async () => { + const { body } = await supertest + .get(`/api/endpoint/resolver/93933?legacyEndpointID=${endpointID}`) + .set(commonHeaders) + .expect(200); + expect(body.pagination.nextAncestor).to.equal(null); + expect(body.pagination.nextEvent).to.equal(null); + expect(body.pagination.nextChild).to.equal(null); + expect(body.children.length).to.equal(0); + expect(body.events.length).to.equal(0); + expect(body.lifecycle.length).to.equal(2); + }); + }); }); } diff --git a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts index 4f17f9db67483..19879f5761ab2 100644 --- a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts +++ b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts @@ -39,6 +39,7 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); } it('should work with a filterQuery', async () => { @@ -53,6 +54,21 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); + }); + it('should work with a filterQuery in KQL format', async () => { + const searchBody = getElasticsearchMetricQuery( + getSearchParams('avg'), + undefined, + '"agent.hostname":"foo"' + ); + const result = await client.search({ + index, + body: searchBody, + }); + expect(result.error).to.not.be.ok(); + expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); }); describe('querying with a groupBy parameter', () => { @@ -65,6 +81,7 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); } it('should work with a filterQuery', async () => { @@ -79,6 +96,7 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); }); }); diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts index bbc766df34dcf..10857caab98e2 100644 --- a/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts @@ -91,12 +91,11 @@ export default ({ getService }: FtrProviderContext) => { model_plot_config: { enabled: true }, }, expected: { - responseCode: 403, + responseCode: 404, responseBody: { - statusCode: 403, - error: 'Forbidden', - message: - '[security_exception] action [cluster:admin/xpack/ml/job/put] is unauthorized for user [ml_viewer]', + statusCode: 404, + error: 'Not Found', + message: 'Not Found', }, }, }, diff --git a/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts index 6a57db1687868..a5cb68d782126 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts @@ -197,8 +197,8 @@ export default ({ getService }: FtrProviderContext) => { requestBody: {}, // Note that the jobs and datafeeds are loaded async so the actual error message is not deterministic. expected: { - responseCode: 403, - error: 'Forbidden', + responseCode: 404, + error: 'Not Found', }, }, ]; diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts index 23ddd3b63a2ef..c42fc95c1bc7f 100644 --- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -84,10 +84,9 @@ export default ({ getService }: FtrProviderContext) => { startDatafeed: false, }, expected: { - responseCode: 403, - error: 'Forbidden', - message: - '[security_exception] action [cluster:monitor/xpack/ml/info/get] is unauthorized for user [ml_unauthorized]', + responseCode: 404, + error: 'Not Found', + message: 'Not Found', }, }, ]; diff --git a/x-pack/test/api_integration/apis/uptime/feature_controls.ts b/x-pack/test/api_integration/apis/uptime/feature_controls.ts index 6d125807e169d..6c566ec7cb23b 100644 --- a/x-pack/test/api_integration/apis/uptime/feature_controls.ts +++ b/x-pack/test/api_integration/apis/uptime/feature_controls.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { PINGS_DATE_RANGE_END, PINGS_DATE_RANGE_START } from './constants'; -import { API_URLS } from '../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../../plugins/uptime/common/constants'; export default function featureControlsTests({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/api_integration/apis/uptime/rest/certs.ts b/x-pack/test/api_integration/apis/uptime/rest/certs.ts index 7510ea3f34d28..a3a15d8f8b014 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/certs.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/certs.ts @@ -8,8 +8,8 @@ import expect from '@kbn/expect'; import moment from 'moment'; import { isRight } from 'fp-ts/lib/Either'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; -import { CertType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; +import { CertType } from '../../../../../plugins/uptime/common/runtime_types'; import { makeChecksWithStatus } from './helper/make_checks'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts b/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts index 5258426cf193c..f343cd1da8788 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts @@ -5,7 +5,7 @@ */ import { FtrProviderContext } from '../../../ftr_provider_context'; import { expectFixtureEql } from './helper/expect_fixture_eql'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; export default function({ getService }: FtrProviderContext) { describe('docCount query', () => { diff --git a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts index ea980721b831b..95caf50d1ca7a 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts @@ -7,9 +7,8 @@ import expect from '@kbn/expect'; import { isRight } from 'fp-ts/lib/Either'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; -import { DynamicSettingsType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; - +import { DynamicSettingsType } from '../../../../../plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../plugins/uptime/common/constants'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts index 3c17370532f91..c3d5849e028ab 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts @@ -7,8 +7,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { makeChecksWithStatus } from './helper/make_checks'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; -import { MonitorSummary } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { MonitorSummary } from '../../../../../plugins/uptime/common/runtime_types'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts index f1e37bff405fd..c5a691312f525 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts @@ -7,8 +7,8 @@ import expect from '@kbn/expect'; import { isRight } from 'fp-ts/lib/Either'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; -import { MonitorSummaryResultType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { MonitorSummaryResultType } from '../../../../../plugins/uptime/common/runtime_types'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; interface ExpectedMonitorStatesPage { response: any; diff --git a/x-pack/test/api_integration/apis/uptime/rest/ping_list.ts b/x-pack/test/api_integration/apis/uptime/rest/ping_list.ts index a261763d5991f..3d754d89cf9be 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/ping_list.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/ping_list.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { isLeft } from 'fp-ts/lib/Either'; import { PathReporter } from 'io-ts/lib/PathReporter'; -import { PingsResponseType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { PingsResponseType } from '../../../../../plugins/uptime/common/runtime_types'; import { FtrProviderContext } from '../../../ftr_provider_context'; function decodePingsResponseData(response: any) { diff --git a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts index 017ef02afe5ea..99e09aa5ce886 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; import { makeChecksWithStatus } from './helper/make_checks'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index e89352118990a..1e6600c7cd2c0 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -78,7 +78,6 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) 'some.non.existent.com', ])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, - '--xpack.alerting.enabled=true', '--xpack.eventLog.logEntries=true', ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, diff --git a/x-pack/test/functional/apps/dashboard/reporting/index.js b/x-pack/test/functional/apps/dashboard/reporting/index.ts similarity index 94% rename from x-pack/test/functional/apps/dashboard/reporting/index.js rename to x-pack/test/functional/apps/dashboard/reporting/index.ts index 99be084d80d74..796e15b4e270f 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/index.js +++ b/x-pack/test/functional/apps/dashboard/reporting/index.ts @@ -5,9 +5,10 @@ */ import expect from '@kbn/expect'; -import path from 'path'; import fs from 'fs'; +import path from 'path'; import { promisify } from 'util'; +import { FtrProviderContext } from '../../../ftr_provider_context'; import { checkIfPngsMatch } from './lib/compare_pngs'; const writeFileAsync = promisify(fs.writeFile); @@ -15,7 +16,7 @@ const mkdirAsync = promisify(fs.mkdir); const REPORTS_FOLDER = path.resolve(__dirname, 'reports'); -export default function({ getService, getPageObjects }) { +export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); const log = getService('log'); @@ -85,14 +86,14 @@ export default function({ getService, getPageObjects }) { describe('Preserve Layout', () => { it('matches baseline report', async function() { - const writeSessionReport = async (name, rawPdf, reportExt) => { + const writeSessionReport = async (name: string, rawPdf: Buffer, reportExt: string) => { const sessionDirectory = path.resolve(REPORTS_FOLDER, 'session'); await mkdirAsync(sessionDirectory, { recursive: true }); const sessionReportPath = path.resolve(sessionDirectory, `${name}.${reportExt}`); await writeFileAsync(sessionReportPath, rawPdf); return sessionReportPath; }; - const getBaselineReportPath = (fileName, reportExt) => { + const getBaselineReportPath = (fileName: string, reportExt: string) => { const baselineFolder = path.resolve(REPORTS_FOLDER, 'baseline'); const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`); log.debug(`getBaselineReportPath (${fullPath})`); diff --git a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.js b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts similarity index 90% rename from x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.js rename to x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts index 13c97a7fce785..b2eb645c8372c 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.js +++ b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts @@ -4,14 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import path from 'path'; -import fs from 'fs'; import { promisify } from 'bluebird'; +import fs from 'fs'; +import path from 'path'; import { comparePngs } from '../../../../../../../test/functional/services/lib/compare_pngs'; -const mkdirAsync = promisify(fs.mkdir); +const mkdirAsync = promisify(fs.mkdir); -export async function checkIfPngsMatch(actualpngPath, baselinepngPath, screenshotsDirectory, log) { +export async function checkIfPngsMatch( + actualpngPath: string, + baselinepngPath: string, + screenshotsDirectory: string, + log: any +) { log.debug(`checkIfpngsMatch: ${actualpngPath} vs ${baselinepngPath}`); // Copy the pngs into the screenshot session directory, as that's where the generated pngs will automatically be // stored. diff --git a/x-pack/test/functional/apps/discover/reporting.js b/x-pack/test/functional/apps/discover/reporting.ts similarity index 96% rename from x-pack/test/functional/apps/discover/reporting.js rename to x-pack/test/functional/apps/discover/reporting.ts index 4aa005fc2db55..7a33e7f5135d4 100644 --- a/x-pack/test/functional/apps/discover/reporting.js +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -5,8 +5,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function({ getService, getPageObjects }) { +export default function({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const esArchiver = getService('esArchiver'); const browser = getService('browser'); diff --git a/x-pack/test/functional/apps/graph/graph.ts b/x-pack/test/functional/apps/graph/graph.ts index 2bbc39969370b..fcf7298c5577a 100644 --- a/x-pack/test/functional/apps/graph/graph.ts +++ b/x-pack/test/functional/apps/graph/graph.ts @@ -76,7 +76,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { } it('should show correct node labels', async function() { - await PageObjects.graph.selectIndexPattern('secrepo*'); + await PageObjects.graph.selectIndexPattern('secrepo'); await buildGraph(); const { nodes } = await PageObjects.graph.getGraphObjects(); const circlesText = nodes.map(({ label }) => label); @@ -120,7 +120,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { it('should create new Graph workspace', async function() { await PageObjects.graph.newGraph(); - await PageObjects.graph.selectIndexPattern('secrepo*'); + await PageObjects.graph.selectIndexPattern('secrepo'); const { nodes, edges } = await PageObjects.graph.getGraphObjects(); expect(nodes).to.be.empty(); expect(edges).to.be.empty(); diff --git a/x-pack/test/functional/apps/uptime/settings.ts b/x-pack/test/functional/apps/uptime/settings.ts index 64b6300e0df63..7a813a5cdfb52 100644 --- a/x-pack/test/functional/apps/uptime/settings.ts +++ b/x-pack/test/functional/apps/uptime/settings.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../legacy/plugins/uptime/common/constants'; -import { DynamicSettings } from '../../../../legacy/plugins/uptime/common/runtime_types'; +import { DynamicSettings } from '../../../../plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../plugins/uptime/common/constants'; import { makeChecks } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; export default ({ getPageObjects, getService }: FtrProviderContext) => { diff --git a/x-pack/test/functional/apps/visualize/reporting.js b/x-pack/test/functional/apps/visualize/reporting.ts similarity index 94% rename from x-pack/test/functional/apps/visualize/reporting.js rename to x-pack/test/functional/apps/visualize/reporting.ts index bc252e1ad4134..5ef954e334d81 100644 --- a/x-pack/test/functional/apps/visualize/reporting.js +++ b/x-pack/test/functional/apps/visualize/reporting.ts @@ -5,8 +5,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function({ getService, getPageObjects }) { +export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); const log = getService('log'); diff --git a/x-pack/test/functional/es_archives/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json index 3fe4f828ba128..d22e3cd3fecdd 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/data.json +++ b/x-pack/test/functional/es_archives/fleet/agents/data.json @@ -11,8 +11,8 @@ "shared_id": "agent1_filebeat", "config_id": "1", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } @@ -30,8 +30,8 @@ "active": true, "shared_id": "agent2_filebeat", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } @@ -49,8 +49,8 @@ "active": true, "shared_id": "agent3_metricbeat", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } @@ -68,8 +68,8 @@ "active": true, "shared_id": "agent4_metricbeat", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } diff --git a/x-pack/test/functional/es_archives/fleet/agents/mappings.json b/x-pack/test/functional/es_archives/fleet/agents/mappings.json index 5d5d373797d4c..409cc3c689eaf 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/mappings.json +++ b/x-pack/test/functional/es_archives/fleet/agents/mappings.json @@ -227,7 +227,7 @@ "type": "date" }, "local_metadata": { - "type": "text" + "type": "flattened" }, "shared_id": { "type": "keyword" @@ -239,7 +239,7 @@ "type": "date" }, "user_provided_metadata": { - "type": "text" + "type": "flattened" }, "version": { "type": "keyword" diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 782d57adea770..4b8c2944ef190 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -19,7 +19,6 @@ import { GraphPageProvider } from './graph_page'; import { GrokDebuggerPageProvider } from './grok_debugger_page'; // @ts-ignore not ts yet import { WatcherPageProvider } from './watcher_page'; -// @ts-ignore not ts yet import { ReportingPageProvider } from './reporting_page'; // @ts-ignore not ts yet import { AccountSettingProvider } from './accountsetting_page'; diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 1bf637c50b0ba..57b2847cc2e50 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -12,6 +12,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont const testSubjects = getService('testSubjects'); const retry = getService('retry'); const find = getService('find'); + const comboBox = getService('comboBox'); const PageObjects = getPageObjects([ 'header', 'common', @@ -107,20 +108,17 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * @param opts.operation - the desired operation ID for the dimension * @param opts.field - the desired field for the dimension */ - async configureDimension(opts: { dimension: string; operation?: string; field?: string }) { + async configureDimension(opts: { dimension: string; operation: string; field: string }) { await find.clickByCssSelector(opts.dimension); - if (opts.operation) { - await find.clickByCssSelector( - `[data-test-subj="lns-indexPatternDimensionIncompatible-${opts.operation}"], - [data-test-subj="lns-indexPatternDimension-${opts.operation}"]` - ); - } + await find.clickByCssSelector( + `[data-test-subj="lns-indexPatternDimensionIncompatible-${opts.operation}"], + [data-test-subj="lns-indexPatternDimension-${opts.operation}"]` + ); - if (opts.field) { - await testSubjects.click('indexPattern-dimension-field'); - await testSubjects.click(`lns-fieldOption-${opts.field}`); - } + const target = await testSubjects.find('indexPattern-dimension-field'); + await comboBox.openOptionsList(target); + await comboBox.setElement(target, opts.field); }, /** diff --git a/x-pack/test/functional/page_objects/reporting_page.js b/x-pack/test/functional/page_objects/reporting_page.ts similarity index 84% rename from x-pack/test/functional/page_objects/reporting_page.js rename to x-pack/test/functional/page_objects/reporting_page.ts index b24ba8cf95d1c..2c20519a8d214 100644 --- a/x-pack/test/functional/page_objects/reporting_page.js +++ b/x-pack/test/functional/page_objects/reporting_page.ts @@ -4,25 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import http, { IncomingMessage } from 'http'; +import { FtrProviderContext } from 'test/functional/ftr_provider_context'; import { parse } from 'url'; -import http from 'http'; -/* - * NOTE: Reporting is a service, not an app. The page objects that are - * important for generating reports belong to the apps that integrate with the - * Reporting service. Eventually, this file should be dissolved across the - * apps that need it for testing their integration. - * Issue: https://github.com/elastic/kibana/issues/52927 - */ -export function ReportingPageProvider({ getService, getPageObjects }) { +export function ReportingPageProvider({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const log = getService('log'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); - const PageObjects = getPageObjects(['common', 'security', 'share', 'timePicker']); + const PageObjects = getPageObjects(['common', 'security' as any, 'share', 'timePicker']); // FIXME: Security PageObject is not Typescript class ReportingPage { - async forceSharedItemsContainerSize({ width }) { + async forceSharedItemsContainerSize({ width }: { width: number }) { await browser.execute(` var el = document.querySelector('[data-shared-items-container]'); el.style.flex="none"; @@ -30,7 +24,7 @@ export function ReportingPageProvider({ getService, getPageObjects }) { `); } - async getReportURL(timeout) { + async getReportURL(timeout: number) { log.debug('getReportURL'); const url = await testSubjects.getAttribute('downloadCompletedReportButton', 'href', timeout); @@ -48,7 +42,7 @@ export function ReportingPageProvider({ getService, getPageObjects }) { `); } - getResponse(url) { + getResponse(url: string): Promise { log.debug(`getResponse for ${url}`); const auth = 'test_user:changeme'; // FIXME not sure why there is no config that can be read for this const headers = { @@ -62,29 +56,30 @@ export function ReportingPageProvider({ getService, getPageObjects }) { hostname: parsedUrl.hostname, path: parsedUrl.path, port: parsedUrl.port, - responseType: 'arraybuffer', headers, }, - res => { + (res: IncomingMessage) => { resolve(res); } ) - .on('error', e => { + .on('error', (e: Error) => { log.error(e); reject(e); }); }); } - async getRawPdfReportData(url) { - const data = []; // List of Buffer objects + async getRawPdfReportData(url: string): Promise { + const data: Buffer[] = []; // List of Buffer objects log.debug(`getRawPdfReportData for ${url}`); return new Promise(async (resolve, reject) => { const response = await this.getResponse(url).catch(reject); - response.on('data', chunk => data.push(chunk)); - response.on('end', () => resolve(Buffer.concat(data))); + if (response) { + response.on('data', (chunk: Buffer) => data.push(chunk)); + response.on('end', () => resolve(Buffer.concat(data))); + } }); } diff --git a/x-pack/test/functional/services/uptime/settings.ts b/x-pack/test/functional/services/uptime/settings.ts index 9719152b62d35..96f5e45ce2ca4 100644 --- a/x-pack/test/functional/services/uptime/settings.ts +++ b/x-pack/test/functional/services/uptime/settings.ts @@ -5,7 +5,7 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; -import { DynamicSettings } from '../../../../legacy/plugins/uptime/common/runtime_types'; +import { DynamicSettings } from '../../../../plugins/uptime/common/runtime_types'; export function UptimeSettingsProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 597f1ad9119b0..9e99c60b4dcb7 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -126,193 +126,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ]); }); - it('should edit an alert', async () => { - const createdAlert = await createAlert({ - alertTypeId: '.index-threshold', - name: generateUniqueKey(), - params: { - aggType: 'count', - termSize: 5, - thresholdComparator: '>', - timeWindowSize: 5, - timeWindowUnit: 'm', - groupBy: 'all', - threshold: [1000, 5000], - index: ['.kibana_1'], - timeField: 'alert', - }, - }); - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResults).to.eql([ - { - name: createdAlert.name, - tagsText: 'foo, bar', - alertType: 'Index threshold', - interval: '1m', - }, - ]); - const editLink = await testSubjects.findAll('alertsTableCell-editLink'); - await editLink[0].click(); - - const updatedAlertName = `Changed Alert Name ${generateUniqueKey()}`; - await testSubjects.setValue('alertNameInput', updatedAlertName, { clearWithKeyboard: true }); - - await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)'); - - const toastTitle = await pageObjects.common.closeToast(); - expect(toastTitle).to.eql(`Updated '${updatedAlertName}'`); - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(updatedAlertName); - - const searchResultsAfterEdit = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResultsAfterEdit).to.eql([ - { - name: updatedAlertName, - tagsText: 'foo, bar', - alertType: 'Index threshold', - interval: '1m', - }, - ]); - }); - - it('should set an alert throttle', async () => { - const alertName = `edit throttle ${generateUniqueKey()}`; - const createdAlert = await createAlert({ - alertTypeId: '.index-threshold', - name: alertName, - params: { - aggType: 'count', - termSize: 5, - thresholdComparator: '>', - timeWindowSize: 5, - timeWindowUnit: 'm', - groupBy: 'all', - threshold: [1000, 5000], - index: ['.kibana_1'], - timeField: 'alert', - }, - }); - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResults).to.eql([ - { - name: createdAlert.name, - tagsText: 'foo, bar', - alertType: 'Index threshold', - interval: '1m', - }, - ]); - - const editLink = await testSubjects.findAll('alertsTableCell-editLink'); - await editLink[0].click(); - - await testSubjects.setValue('throttleInput', '1', { clearWithKeyboard: true }); - - await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)'); - - expect(await pageObjects.common.closeToast()).to.eql(`Updated '${createdAlert.name}'`); - - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - await (await testSubjects.findAll('alertsTableCell-editLink'))[0].click(); - const throttleInput = await testSubjects.find('throttleInput'); - expect(await throttleInput.getAttribute('value')).to.eql('1'); - }); - - it('should unset an alert throttle', async () => { - const alertName = `edit throttle ${generateUniqueKey()}`; - const createdAlert = await createAlert({ - alertTypeId: '.index-threshold', - name: alertName, - throttle: '10m', - params: { - aggType: 'count', - termSize: 5, - thresholdComparator: '>', - timeWindowSize: 5, - timeWindowUnit: 'm', - groupBy: 'all', - threshold: [1000, 5000], - index: ['.kibana_1'], - timeField: 'alert', - }, - }); - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResults).to.eql([ - { - name: createdAlert.name, - tagsText: 'foo, bar', - alertType: 'Index threshold', - interval: '1m', - }, - ]); - - const editLink = await testSubjects.findAll('alertsTableCell-editLink'); - await editLink[0].click(); - - const throttleInputToUnsetValue = await testSubjects.find('throttleInput'); - - expect(await throttleInputToUnsetValue.getAttribute('value')).to.eql('10'); - await throttleInputToUnsetValue.click(); - await throttleInputToUnsetValue.clearValueWithKeyboard(); - - expect(await throttleInputToUnsetValue.getAttribute('value')).to.eql(''); - - await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)'); - - expect(await pageObjects.common.closeToast()).to.eql(`Updated '${createdAlert.name}'`); - - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - await (await testSubjects.findAll('alertsTableCell-editLink'))[0].click(); - const throttleInput = await testSubjects.find('throttleInput'); - expect(await throttleInput.getAttribute('value')).to.eql(''); - }); - - it('should reset alert when canceling an edit', async () => { - const createdAlert = await createAlert({ - alertTypeId: '.index-threshold', - name: generateUniqueKey(), - params: { - aggType: 'count', - termSize: 5, - thresholdComparator: '>', - timeWindowSize: 5, - timeWindowUnit: 'm', - groupBy: 'all', - threshold: [1000, 5000], - index: ['.kibana_1'], - timeField: 'alert', - }, - }); - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - const editLink = await testSubjects.findAll('alertsTableCell-editLink'); - await editLink[0].click(); - - const updatedAlertName = `Changed Alert Name ${generateUniqueKey()}`; - await testSubjects.setValue('alertNameInput', updatedAlertName); - - await testSubjects.click('cancelSaveEditedAlertButton'); - await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedAlertButton"]'); - - const editLinkPostCancel = await testSubjects.findAll('alertsTableCell-editLink'); - await editLinkPostCancel[0].click(); - - const nameInputAfterCancel = await testSubjects.find('alertNameInput'); - const textAfterCancel = await nameInputAfterCancel.getAttribute('value'); - expect(textAfterCancel).to.eql(createdAlert.name); - }); - it('should search for tags', async () => { const createdAlert = await createAlert(); await pageObjects.common.navigateToApp('triggersActions'); diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index a620b1d953376..71b22a336f6b9 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -50,8 +50,6 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { `--elasticsearch.hosts=https://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, `--plugin-path=${join(__dirname, 'fixtures', 'plugins', 'alerts')}`, - '--xpack.actions.enabled=true', - '--xpack.alerting.enabled=true', `--xpack.actions.preconfigured=${JSON.stringify([ { id: 'my-slack1', diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts index c5f3e65581df9..9622715e87e55 100644 --- a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts +++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts @@ -40,7 +40,7 @@ export const logEventRoute = (router: IRouter, eventLogger: IEventLogger, logger } catch (ex) { logger.info(`log event error: ${ex}`); await context.core.savedObjects.client.create('event_log_test', {}, { id }); - logger.info(`created saved object`); + logger.info(`created saved object ${id}`); } eventLogger.logEvent(event); logger.info(`logged`); diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/kibana.json b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/kibana.json new file mode 100644 index 0000000000000..416ef7fa34591 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "sample_task_plugin", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack"], + "requiredPlugins": ["taskManager"], + "server": true, + "ui": false +} diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/package.json b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/package.json new file mode 100644 index 0000000000000..c8d47decd94c1 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/package.json @@ -0,0 +1,18 @@ +{ + "name": "sample_task_plugin", + "version": "1.0.0", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "main": "target/test/plugin_api_integration/plugins/sample_task_plugin", + "scripts": { + "kbn": "node ../../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.7.2" + }, + "license": "Apache-2.0", + "dependencies": {} +} diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/index.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/index.ts new file mode 100644 index 0000000000000..77233f463734a --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { SampleTaskManagerFixturePlugin } from './plugin'; + +export const plugin = () => new SampleTaskManagerFixturePlugin(); diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts new file mode 100644 index 0000000000000..1fee2decbcba9 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts @@ -0,0 +1,252 @@ +/* + * 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 } from '@kbn/config-schema'; +import { + RequestHandlerContext, + KibanaRequest, + KibanaResponseFactory, + IKibanaResponse, + IRouter, + CoreSetup, +} from 'kibana/server'; +import { EventEmitter } from 'events'; +import { TaskManagerStartContract } from '../../../../../plugins/task_manager/server'; + +const scope = 'testing'; +const taskManagerQuery = { + bool: { + filter: { + bool: { + must: [ + { + term: { + 'task.scope': scope, + }, + }, + ], + }, + }, + }, +}; + +export function initRoutes( + router: IRouter, + core: CoreSetup, + taskManagerStart: Promise, + taskTestingEvents: EventEmitter +) { + async function ensureIndexIsRefreshed() { + return await core.elasticsearch.adminClient.callAsInternalUser('indices.refresh', { + index: '.kibana_task_manager', + }); + } + + router.post( + { + path: `/api/sample_tasks/schedule`, + validate: { + body: schema.object({ + task: schema.object({ + taskType: schema.string(), + schedule: schema.maybe( + schema.object({ + interval: schema.string(), + }) + ), + interval: schema.maybe(schema.string()), + params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + state: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + id: schema.maybe(schema.string()), + }), + }), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + try { + const taskManager = await taskManagerStart; + const { task: taskFields } = req.body; + const task = { + ...taskFields, + scope: [scope], + }; + + const taskResult = await taskManager.schedule(task, { req }); + + return res.ok({ body: taskResult }); + } catch (err) { + return res.internalError({ body: err }); + } + } + ); + + router.post( + { + path: `/api/sample_tasks/run_now`, + validate: { + body: schema.object({ + task: schema.object({ + id: schema.string({}), + }), + }), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + const { + task: { id }, + } = req.body; + try { + const taskManager = await taskManagerStart; + return res.ok({ body: await taskManager.runNow(id) }); + } catch (err) { + return res.ok({ body: { id, error: `${err}` } }); + } + } + ); + + router.post( + { + path: `/api/sample_tasks/ensure_scheduled`, + validate: { + body: schema.object({ + task: schema.object({ + taskType: schema.string(), + params: schema.object({}), + state: schema.maybe(schema.object({})), + id: schema.maybe(schema.string()), + }), + }), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + try { + const { task: taskFields } = req.body; + const task = { + ...taskFields, + scope: [scope], + }; + + const taskManager = await taskManagerStart; + const taskResult = await taskManager.ensureScheduled(task, { req }); + + return res.ok({ body: taskResult }); + } catch (err) { + return res.ok({ body: err }); + } + } + ); + + router.post( + { + path: `/api/sample_tasks/event`, + validate: { + body: schema.object({ + event: schema.string(), + data: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + }), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + try { + const { event, data } = req.body; + taskTestingEvents.emit(event, data); + return res.ok({ body: event }); + } catch (err) { + return res.ok({ body: err }); + } + } + ); + + router.get( + { + path: `/api/sample_tasks`, + validate: {}, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + try { + const taskManager = await taskManagerStart; + return res.ok({ + body: await taskManager.fetch({ + query: taskManagerQuery, + }), + }); + } catch (err) { + return res.ok({ body: err }); + } + } + ); + + router.get( + { + path: `/api/sample_tasks/task/{taskId}`, + validate: { + params: schema.object({ + taskId: schema.string(), + }), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + try { + await ensureIndexIsRefreshed(); + const taskManager = await taskManagerStart; + return res.ok({ body: await taskManager.get(req.params.taskId) }); + } catch (err) { + return res.ok({ body: err }); + } + return res.ok({ body: {} }); + } + ); + + router.delete( + { + path: `/api/sample_tasks`, + validate: {}, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + try { + let tasksFound = 0; + const taskManager = await taskManagerStart; + do { + const { docs: tasks } = await taskManager.fetch({ + query: taskManagerQuery, + }); + tasksFound = tasks.length; + await Promise.all(tasks.map(task => taskManager.remove(task.id))); + } while (tasksFound > 0); + return res.ok({ body: 'OK' }); + } catch (err) { + return res.ok({ body: err }); + } + } + ); +} diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts new file mode 100644 index 0000000000000..508d58b8f0ca9 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts @@ -0,0 +1,164 @@ +/* + * 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 { Plugin, CoreSetup, CoreStart } from 'kibana/server'; +import { EventEmitter } from 'events'; +import { Subject } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { initRoutes } from './init_routes'; +import { + TaskManagerSetupContract, + TaskManagerStartContract, + ConcreteTaskInstance, +} from '../../../../../plugins/task_manager/server'; +import { DEFAULT_MAX_WORKERS } from '../../../../../plugins/task_manager/server/config'; + +// this plugin's dependendencies +export interface SampleTaskManagerFixtureSetupDeps { + taskManager: TaskManagerSetupContract; +} +export interface SampleTaskManagerFixtureStartDeps { + taskManager: TaskManagerStartContract; +} + +export class SampleTaskManagerFixturePlugin + implements + Plugin { + taskManagerStart$: Subject = new Subject(); + taskManagerStart: Promise = this.taskManagerStart$ + .pipe(first()) + .toPromise(); + + public setup(core: CoreSetup, { taskManager }: SampleTaskManagerFixtureSetupDeps) { + const taskTestingEvents = new EventEmitter(); + taskTestingEvents.setMaxListeners(DEFAULT_MAX_WORKERS * 2); + + const defaultSampleTaskConfig = { + timeout: '1m', + // This task allows tests to specify its behavior (whether it reschedules itself, whether it errors, etc) + // taskInstance.params has the following optional fields: + // nextRunMilliseconds: number - If specified, the run method will return a runAt that is now + nextRunMilliseconds + // failWith: string - If specified, the task will throw an error with the specified message + // failOn: number - If specified, the task will only throw the `failWith` error when `count` equals to the failOn value + // waitForParams : boolean - should the task stall ands wait to receive params asynchronously before using the default params + // waitForEvent : string - if provided, the task will stall (after completing the run) and wait for an asyn event before completing + createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => ({ + async run() { + const { params, state, id } = taskInstance; + const prevState = state || { count: 0 }; + + const count = (prevState.count || 0) + 1; + + const runParams = { + ...params, + // if this task requires custom params provided async - wait for them + ...(params.waitForParams ? await once(taskTestingEvents, id) : {}), + }; + + if (runParams.failWith) { + if (!runParams.failOn || (runParams.failOn && count === runParams.failOn)) { + throw new Error(runParams.failWith); + } + } + + await core.elasticsearch.adminClient.callAsInternalUser('index', { + index: '.kibana_task_manager_test_result', + body: { + type: 'task', + taskId: taskInstance.id, + params: JSON.stringify(runParams), + state: JSON.stringify(state), + ranAt: new Date(), + }, + refresh: true, + }); + + // Stall task run until a certain event is triggered + if (runParams.waitForEvent) { + await once(taskTestingEvents, runParams.waitForEvent); + } + + return { + state: { count }, + runAt: millisecondsFromNow(runParams.nextRunMilliseconds), + }; + }, + }), + }; + + taskManager.registerTaskDefinitions({ + sampleTask: { + ...defaultSampleTaskConfig, + type: 'sampleTask', + title: 'Sample Task', + description: 'A sample task for testing the task_manager.', + }, + singleAttemptSampleTask: { + ...defaultSampleTaskConfig, + type: 'singleAttemptSampleTask', + title: 'Failing Sample Task', + description: + 'A sample task for testing the task_manager that fails on the first attempt to run.', + // fail after the first failed run + maxAttempts: 1, + }, + }); + + taskManager.addMiddleware({ + async beforeSave({ taskInstance, ...opts }) { + const modifiedInstance = { + ...taskInstance, + params: { + originalParams: taskInstance.params, + superFly: 'My middleware param!', + }, + }; + + return { + ...opts, + taskInstance: modifiedInstance, + }; + }, + + async beforeRun({ taskInstance, ...opts }) { + return { + ...opts, + taskInstance: { + ...taskInstance, + params: taskInstance.params.originalParams, + }, + }; + }, + + async beforeMarkRunning(context) { + return context; + }, + }); + initRoutes(core.http.createRouter(), core, this.taskManagerStart, taskTestingEvents); + } + + public start(core: CoreStart, { taskManager }: SampleTaskManagerFixtureStartDeps) { + this.taskManagerStart$.next(taskManager); + this.taskManagerStart$.complete(); + } + public stop() {} +} + +function millisecondsFromNow(ms: number) { + if (!ms) { + return; + } + + const dt = new Date(); + dt.setTime(dt.getTime() + ms); + return dt; +} + +const once = function(emitter: EventEmitter, event: string): Promise> { + return new Promise(resolve => { + emitter.once(event, data => resolve(data || {})); + }); +}; diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/index.js b/x-pack/test/plugin_api_integration/plugins/task_manager/index.js deleted file mode 100644 index e5b645367b8b7..0000000000000 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/index.js +++ /dev/null @@ -1,150 +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. - */ - -const { DEFAULT_MAX_WORKERS } = require('../../../../plugins/task_manager/server/config.ts'); -const { EventEmitter } = require('events'); - -import { initRoutes } from './init_routes'; - -const once = function(emitter, event) { - return new Promise(resolve => { - emitter.once(event, data => resolve(data || {})); - }); -}; - -export default function TaskTestingAPI(kibana) { - const taskTestingEvents = new EventEmitter(); - taskTestingEvents.setMaxListeners(DEFAULT_MAX_WORKERS * 2); - - return new kibana.Plugin({ - name: 'sampleTask', - require: ['elasticsearch', 'task_manager'], - - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - - init(server) { - const taskManager = { - ...server.newPlatform.setup.plugins.taskManager, - ...server.newPlatform.start.plugins.taskManager, - }; - const legacyTaskManager = server.plugins.task_manager; - - const defaultSampleTaskConfig = { - timeout: '1m', - // This task allows tests to specify its behavior (whether it reschedules itself, whether it errors, etc) - // taskInstance.params has the following optional fields: - // nextRunMilliseconds: number - If specified, the run method will return a runAt that is now + nextRunMilliseconds - // failWith: string - If specified, the task will throw an error with the specified message - // failOn: number - If specified, the task will only throw the `failWith` error when `count` equals to the failOn value - // waitForParams : boolean - should the task stall ands wait to receive params asynchronously before using the default params - // waitForEvent : string - if provided, the task will stall (after completing the run) and wait for an asyn event before completing - createTaskRunner: ({ taskInstance }) => ({ - async run() { - const { params, state, id } = taskInstance; - const prevState = state || { count: 0 }; - - const count = (prevState.count || 0) + 1; - - const runParams = { - ...params, - // if this task requires custom params provided async - wait for them - ...(params.waitForParams ? await once(taskTestingEvents, id) : {}), - }; - - if (runParams.failWith) { - if (!runParams.failOn || (runParams.failOn && count === runParams.failOn)) { - throw new Error(runParams.failWith); - } - } - - const callCluster = server.plugins.elasticsearch.getCluster('admin') - .callWithInternalUser; - await callCluster('index', { - index: '.kibana_task_manager_test_result', - body: { - type: 'task', - taskId: taskInstance.id, - params: JSON.stringify(runParams), - state: JSON.stringify(state), - ranAt: new Date(), - }, - refresh: true, - }); - - // Stall task run until a certain event is triggered - if (runParams.waitForEvent) { - await once(taskTestingEvents, runParams.waitForEvent); - } - - return { - state: { count }, - runAt: millisecondsFromNow(runParams.nextRunMilliseconds), - }; - }, - }), - }; - - taskManager.registerTaskDefinitions({ - sampleTask: { - ...defaultSampleTaskConfig, - title: 'Sample Task', - description: 'A sample task for testing the task_manager.', - }, - singleAttemptSampleTask: { - ...defaultSampleTaskConfig, - title: 'Failing Sample Task', - description: - 'A sample task for testing the task_manager that fails on the first attempt to run.', - // fail after the first failed run - maxAttempts: 1, - }, - }); - - taskManager.addMiddleware({ - async beforeSave({ taskInstance, ...opts }) { - const modifiedInstance = { - ...taskInstance, - params: { - originalParams: taskInstance.params, - superFly: 'My middleware param!', - }, - }; - - return { - ...opts, - taskInstance: modifiedInstance, - }; - }, - - async beforeRun({ taskInstance, ...opts }) { - return { - ...opts, - taskInstance: { - ...taskInstance, - params: taskInstance.params.originalParams, - }, - }; - }, - }); - - initRoutes(server, taskManager, legacyTaskManager, taskTestingEvents); - }, - }); -} - -function millisecondsFromNow(ms) { - if (!ms) { - return; - } - - const dt = new Date(); - dt.setTime(dt.getTime() + ms); - return dt; -} diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js deleted file mode 100644 index 785fbed341423..0000000000000 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js +++ /dev/null @@ -1,236 +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 Joi from 'joi'; - -const scope = 'testing'; -const taskManagerQuery = { - bool: { - filter: { - bool: { - must: [ - { - term: { - 'task.scope': scope, - }, - }, - ], - }, - }, - }, -}; - -export function initRoutes(server, taskManager, legacyTaskManager, taskTestingEvents) { - const callCluster = server.plugins.elasticsearch.getCluster('admin').callWithInternalUser; - - async function ensureIndexIsRefreshed() { - return await callCluster('indices.refresh', { - index: '.kibana_task_manager', - }); - } - - server.route({ - path: '/api/sample_tasks/schedule', - method: 'POST', - config: { - validate: { - payload: Joi.object({ - task: Joi.object({ - taskType: Joi.string().required(), - schedule: Joi.object({ - interval: Joi.string(), - }).optional(), - interval: Joi.string().optional(), - params: Joi.object().required(), - state: Joi.object().optional(), - id: Joi.string().optional(), - }), - }), - }, - }, - async handler(request) { - try { - const { task: taskFields } = request.payload; - const task = { - ...taskFields, - scope: [scope], - }; - - const taskResult = await taskManager.schedule(task, { request }); - - return taskResult; - } catch (err) { - return err; - } - }, - }); - - /* - Schedule using legacy Api - */ - server.route({ - path: '/api/sample_tasks/schedule_legacy', - method: 'POST', - config: { - validate: { - payload: Joi.object({ - task: Joi.object({ - taskType: Joi.string().required(), - schedule: Joi.object({ - interval: Joi.string(), - }).optional(), - interval: Joi.string().optional(), - params: Joi.object().required(), - state: Joi.object().optional(), - id: Joi.string().optional(), - }), - }), - }, - }, - async handler(request) { - try { - const { task: taskFields } = request.payload; - const task = { - ...taskFields, - scope: [scope], - }; - - const taskResult = await legacyTaskManager.schedule(task, { request }); - - return taskResult; - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/sample_tasks/run_now', - method: 'POST', - config: { - validate: { - payload: Joi.object({ - task: Joi.object({ - id: Joi.string().optional(), - }), - }), - }, - }, - async handler(request) { - const { - task: { id }, - } = request.payload; - try { - return await taskManager.runNow(id); - } catch (err) { - return { id, error: `${err}` }; - } - }, - }); - - server.route({ - path: '/api/sample_tasks/ensure_scheduled', - method: 'POST', - config: { - validate: { - payload: Joi.object({ - task: Joi.object({ - taskType: Joi.string().required(), - params: Joi.object().required(), - state: Joi.object().optional(), - id: Joi.string().optional(), - }), - }), - }, - }, - async handler(request) { - try { - const { task: taskFields } = request.payload; - const task = { - ...taskFields, - scope: [scope], - }; - - const taskResult = await taskManager.ensureScheduled(task, { request }); - - return taskResult; - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/sample_tasks/event', - method: 'POST', - config: { - validate: { - payload: Joi.object({ - event: Joi.string().required(), - data: Joi.object() - .optional() - .default({}), - }), - }, - }, - async handler(request) { - try { - const { event, data } = request.payload; - taskTestingEvents.emit(event, data); - return { event }; - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/sample_tasks', - method: 'GET', - async handler() { - try { - return taskManager.fetch({ - query: taskManagerQuery, - }); - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/sample_tasks/task/{taskId}', - method: 'GET', - async handler(request) { - try { - await ensureIndexIsRefreshed(); - return await taskManager.get(request.params.taskId); - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/sample_tasks', - method: 'DELETE', - async handler() { - try { - let tasksFound = 0; - do { - const { docs: tasks } = await taskManager.fetch({ - query: taskManagerQuery, - }); - tasksFound = tasks.length; - await Promise.all(tasks.map(task => taskManager.remove(task.id))); - } while (tasksFound > 0); - return 'OK'; - } catch (err) { - return err; - } - }, - }); -} diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/package.json b/x-pack/test/plugin_api_integration/plugins/task_manager/package.json deleted file mode 100644 index ec63c512e9cd7..0000000000000 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "sample_task_plugin", - "version": "1.0.0", - "kibana": { - "version": "kibana", - "templateVersion": "1.0.0" - }, - "license": "Apache-2.0", - "dependencies": { - "joi": "^13.5.2" - } -} diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts index e5b840b335846..f3a3d58336b1d 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts @@ -19,7 +19,9 @@ export default function({ getService }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); - describe('Event Log public API', () => { + // FLAKY: https://github.com/elastic/kibana/issues/64723 + // FLAKY: https://github.com/elastic/kibana/issues/64812 + describe.skip('Event Log public API', () => { it('should allow querying for events by Saved Object', async () => { const id = uuid.v4(); @@ -203,6 +205,7 @@ export default function({ getService }: FtrProviderContext) { kibana: { saved_objects: [ { + rel: 'primary', namespace: 'default', type: 'event_log_test', id, diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts index 31668e8345275..361d80aaedd41 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts @@ -101,7 +101,7 @@ export default function({ getService }: FtrProviderContext) { const eventId = uuid.v4(); const event: IEvent = { event: { action: 'action1', provider: 'provider4' }, - kibana: { saved_objects: [{ type: 'event_log_test', id: eventId }] }, + kibana: { saved_objects: [{ rel: 'primary', type: 'event_log_test', id: eventId }] }, }; await logTestEvent(eventId, event); diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js index e8f976d5ae6e3..00cefa42711c9 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js @@ -11,7 +11,7 @@ import supertestAsPromised from 'supertest-as-promised'; const { task: { properties: taskManagerIndexMapping }, -} = require('../../../../legacy/plugins/task_manager/server/mappings.json'); +} = require('../../../../plugins/task_manager/server/saved_objects/mappings.json'); const { DEFAULT_MAX_WORKERS, @@ -90,15 +90,6 @@ export default function({ getService }) { .then(response => response.body); } - function scheduleTaskUsingLegacyApi(task) { - return supertest - .post('/api/sample_tasks/schedule_legacy') - .set('kbn-xsrf', 'xxx') - .send({ task }) - .expect(200) - .then(response => response.body); - } - function runTaskNow(task) { return supertest .post('/api/sample_tasks/run_now') @@ -587,15 +578,5 @@ export default function({ getService }) { expect(getTaskById(tasks, longRunningTask.id).state.count).to.eql(1); }); }); - - it('should retain the legacy api until v8.0.0', async () => { - const result = await scheduleTaskUsingLegacyApi({ - id: 'task-with-legacy-api', - taskType: 'sampleTask', - params: {}, - }); - - expect(result.id).to.be('task-with-legacy-api'); - }); }); } diff --git a/yarn.lock b/yarn.lock index ee00ef283f07c..94e6a0a11aa99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4798,6 +4798,11 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== +"@types/set-value@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/set-value/-/set-value-2.0.0.tgz#63d386b103926dcf49b50e16e0f6dd49983046be" + integrity sha512-k8dCJEC80F/mbsIOZ5Hj3YSzTVVVBwMdtP/M9Rtc2TM4F5etVd+2UG8QUiAUfbXm4fABedL2tBZnrBheY7UwpA== + "@types/shot@*": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/shot/-/shot-4.0.0.tgz#7545500c489b65c69b5bc5446ba4fef3bd26af92" @@ -26910,6 +26915,13 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" +set-value@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90" + integrity sha512-npjkVoz+ank0zjlV9F47Fdbjfj/PfXyVhZvGALWsyIYU/qrMzpi6avjKW3/7KeSU2Df3I46BrN1xOI1+6vW0hA== + dependencies: + is-plain-object "^2.0.4" + setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"