diff --git a/.buildkite/scripts/bootstrap.sh b/.buildkite/scripts/bootstrap.sh index c80f215052a26..413c56e2e8109 100755 --- a/.buildkite/scripts/bootstrap.sh +++ b/.buildkite/scripts/bootstrap.sh @@ -19,7 +19,7 @@ if ! yarn kbn bootstrap; then fi if [[ "$DISABLE_BOOTSTRAP_VALIDATION" != "true" ]]; then - verify_no_git_changes 'yarn kbn bootstrap' + check_for_changed_files 'yarn kbn bootstrap' fi ### diff --git a/.buildkite/scripts/common/util.sh b/.buildkite/scripts/common/util.sh index 4a81506200cfa..d6aa988a4c696 100755 --- a/.buildkite/scripts/common/util.sh +++ b/.buildkite/scripts/common/util.sh @@ -14,17 +14,46 @@ is_pr() { false } -verify_no_git_changes() { +check_for_changed_files() { RED='\033[0;31m' YELLOW='\033[0;33m' C_RESET='\033[0m' # Reset color + SHOULD_AUTO_COMMIT_CHANGES="${2:-}" GIT_CHANGES="$(git ls-files --modified -- . ':!:.bazelrc')" + if [ "$GIT_CHANGES" ]; then - echo -e "\n${RED}ERROR: '$1' caused changes to the following files:${C_RESET}\n" - echo -e "$GIT_CHANGES\n" - echo -e "\n${YELLOW}TO FIX: Run '$1' locally, commit the changes and push to your branch${C_RESET}\n" - exit 1 + if [[ "$SHOULD_AUTO_COMMIT_CHANGES" == "true" && "${BUILDKITE_PULL_REQUEST:-}" ]]; then + NEW_COMMIT_MESSAGE="[CI] Auto-commit changed files from '$1'" + PREVIOUS_COMMIT_MESSAGE="$(git log -1 --pretty=%B)" + + if [[ "$NEW_COMMIT_MESSAGE" == "$PREVIOUS_COMMIT_MESSAGE" ]]; then + echo -e "\n${RED}ERROR: '$1' caused changes to the following files:${C_RESET}\n" + echo -e "$GIT_CHANGES\n" + echo -e "CI already attempted to commit these changes, but the file(s) seem to have changed again." + echo -e "Please review and fix manually." + exit 1 + fi + + echo "'$1' caused changes to the following files:" + echo "$GIT_CHANGES" + echo "" + echo "Auto-committing these changes now. A new build should start soon if successful." + + git config --global user.name kibanamachine + git config --global user.email '42973632+kibanamachine@users.noreply.github.com' + gh pr checkout "${BUILDKITE_PULL_REQUEST}" + git add -u -- . ':!.bazelrc' + + git commit -m "$NEW_COMMIT_MESSAGE" + git push + exit 1 + else + echo -e "\n${RED}ERROR: '$1' caused changes to the following files:${C_RESET}\n" + echo -e "$GIT_CHANGES\n" + echo -e "\n${YELLOW}TO FIX: Run '$1' locally, commit the changes and push to your branch${C_RESET}\n" + exit 1 + fi fi } diff --git a/.buildkite/scripts/steps/checks/bazel_packages.sh b/.buildkite/scripts/steps/checks/bazel_packages.sh index 85268bdcb0f06..ca82c6fef609e 100755 --- a/.buildkite/scripts/steps/checks/bazel_packages.sh +++ b/.buildkite/scripts/steps/checks/bazel_packages.sh @@ -5,4 +5,6 @@ set -euo pipefail source .buildkite/scripts/common/util.sh echo --- Check Bazel Packages Manifest -node scripts/generate packages_build_manifest --validate +node scripts/generate packages_build_manifest + +check_for_changed_files 'node scripts/generate packages_build_manifest' true diff --git a/.buildkite/scripts/steps/checks/kbn_pm_dist.sh b/.buildkite/scripts/steps/checks/kbn_pm_dist.sh index 61281bc136311..6f75bd9ea4e29 100755 --- a/.buildkite/scripts/steps/checks/kbn_pm_dist.sh +++ b/.buildkite/scripts/steps/checks/kbn_pm_dist.sh @@ -7,4 +7,4 @@ source .buildkite/scripts/common/util.sh echo "--- Building kbn-pm distributable" yarn kbn run build -i @kbn/pm -verify_no_git_changes 'yarn kbn run build -i @kbn/pm' +check_for_changed_files 'yarn kbn run build -i @kbn/pm' true diff --git a/.buildkite/scripts/steps/checks/plugin_list_docs.sh b/.buildkite/scripts/steps/checks/plugin_list_docs.sh index b422e478aaf7d..04797887a9006 100755 --- a/.buildkite/scripts/steps/checks/plugin_list_docs.sh +++ b/.buildkite/scripts/steps/checks/plugin_list_docs.sh @@ -7,4 +7,4 @@ source .buildkite/scripts/common/util.sh echo "--- Building plugin list docs" node scripts/build_plugin_list_docs -verify_no_git_changes 'node scripts/build_plugin_list_docs' +check_for_changed_files 'node scripts/build_plugin_list_docs' true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1ee6089df8c5f..1ccad2a3ac36d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -118,7 +118,11 @@ /x-pack/test/api_integration/apis/monitoring @elastic/infra-monitoring-ui # Fleet +/fleet_packages.json @elastic/fleet /x-pack/plugins/fleet/ @elastic/fleet +/x-pack/test/fleet_api_integration @elastic/fleet +/x-pack/test/fleet_cypress @elastic/fleet +/x-pack/test/fleet_functional @elastic/fleet # APM /x-pack/plugins/apm/ @elastic/apm-ui diff --git a/config/kibana.yml b/config/kibana.yml index 8ca8eb673c276..50ddad9a4b32a 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -62,6 +62,10 @@ # must be a positive integer. #elasticsearch.requestTimeout: 30000 +# The maximum number of sockets that can be used for communications with elasticsearch. +# Defaults to `Infinity`. +#elasticsearch.maxSockets: 1024 + # Specifies whether Kibana should use compression for communications with elasticsearch # Defaults to `false`. #elasticsearch.compression: false diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md index e7b65fc9dfa56..2a8a99c17ad63 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md @@ -9,7 +9,7 @@ Configuration options to be used to create a [cluster client](./kibana-plugin-co Signature: ```typescript -export declare type ElasticsearchClientConfig = Pick & { +export declare type ElasticsearchClientConfig = Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; ssl?: Partial; diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.maxsockets.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.maxsockets.md new file mode 100644 index 0000000000000..64403f0cad543 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.maxsockets.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchConfig](./kibana-plugin-core-server.elasticsearchconfig.md) > [maxSockets](./kibana-plugin-core-server.elasticsearchconfig.maxsockets.md) + +## ElasticsearchConfig.maxSockets property + +The maximum number of sockets that can be used for communications with elasticsearch. + +Signature: + +```typescript +readonly maxSockets: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md index 3e9bb43c98501..593836664d5bf 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md @@ -28,6 +28,7 @@ export declare class ElasticsearchConfig | [healthCheckDelay](./kibana-plugin-core-server.elasticsearchconfig.healthcheckdelay.md) | | Duration | The interval between health check requests Kibana sends to the Elasticsearch. | | [hosts](./kibana-plugin-core-server.elasticsearchconfig.hosts.md) | | string\[\] | Hosts that the client will connect to. If sniffing is enabled, this list will be used as seeds to discover the rest of your cluster. | | [ignoreVersionMismatch](./kibana-plugin-core-server.elasticsearchconfig.ignoreversionmismatch.md) | | boolean | Whether to allow kibana to connect to a non-compatible elasticsearch node. | +| [maxSockets](./kibana-plugin-core-server.elasticsearchconfig.maxsockets.md) | | number | The maximum number of sockets that can be used for communications with elasticsearch. | | [password?](./kibana-plugin-core-server.elasticsearchconfig.password.md) | | string | (Optional) If Elasticsearch is protected with basic authentication, this setting provides the password that the Kibana server uses to perform its administrative functions. | | [pingTimeout](./kibana-plugin-core-server.elasticsearchconfig.pingtimeout.md) | | Duration | Timeout after which PING HTTP request will be aborted and retried. | | [requestHeadersWhitelist](./kibana-plugin-core-server.elasticsearchconfig.requestheaderswhitelist.md) | | string\[\] | List of Kibana client-side headers to send to Elasticsearch when request scoped cluster client is used. If this is an empty array then \*no\* client-side will be sent. | diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 3a1e0f1a7f4ff..2b36e1fb66185 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -90,6 +90,10 @@ enforce even rudimentary CSP rules, though {kib} is still accessible. This configuration is effectively ignored when <> is enabled. *Default: `true`* +|[[elasticsearch-maxSockets]] `elasticsearch.maxSockets` + | The maximum number of sockets that can be used for communications with elasticsearch. +*Default: `Infinity`* + | `elasticsearch.customHeaders:` | Header names and values to send to {es}. Any custom headers cannot be overwritten by client-side headers, regardless of the diff --git a/package.json b/package.json index 60f74602e9368..a2a79a3a5d350 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "@elastic/apm-rum": "^5.10.2", "@elastic/apm-rum-react": "^1.3.4", "@elastic/apm-synthtrace": "link:bazel-bin/packages/elastic-apm-synthtrace", - "@elastic/charts": "43.1.1", + "@elastic/charts": "45.0.1", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.2.0-canary.1", "@elastic/ems-client": "8.1.0", @@ -410,7 +410,7 @@ "usng.js": "^0.4.5", "utility-types": "^3.10.0", "uuid": "3.3.2", - "vega": "^5.21.0", + "vega": "^5.22.0", "vega-interpreter": "^1.0.4", "vega-lite": "^5.2.0", "vega-schema-url-parser": "^2.2.0", diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index fe4b04548244a..e420b4ea65947 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -236,6 +236,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: mainFields: ['browser', 'main'], alias: { core_app_image_assets: Path.resolve(worker.repoRoot, 'src/core/public/core_app/images'), + vega: Path.resolve(worker.repoRoot, 'node_modules/vega/build-es5/vega.js'), }, symlinks: false, }, diff --git a/packages/kbn-securitysolution-utils/src/path_validations/index.ts b/packages/kbn-securitysolution-utils/src/path_validations/index.ts index 82d2cc3151b90..27d2f31d76d9a 100644 --- a/packages/kbn-securitysolution-utils/src/path_validations/index.ts +++ b/packages/kbn-securitysolution-utils/src/path_validations/index.ts @@ -28,7 +28,9 @@ export const enum OperatingSystem { WINDOWS = 'windows', } -export type TrustedAppEntryTypes = 'match' | 'wildcard'; +export type EntryTypes = 'match' | 'wildcard' | 'match_any'; +export type TrustedAppEntryTypes = Extract; + /* * regex to match executable names * starts matching from the eol of the path @@ -82,7 +84,7 @@ export const hasSimpleExecutableName = ({ value, }: { os: OperatingSystem; - type: TrustedAppEntryTypes; + type: EntryTypes; value: string; }): boolean => { if (type === 'wildcard') { @@ -99,7 +101,7 @@ export const isPathValid = ({ }: { os: OperatingSystem; field: ConditionEntryField | 'file.path.text'; - type: TrustedAppEntryTypes; + type: EntryTypes; value: string; }): boolean => { if (field === ConditionEntryField.PATH || field === 'file.path.text') { diff --git a/src/core/server/elasticsearch/client/client_config.test.ts b/src/core/server/elasticsearch/client/client_config.test.ts index cf5a15ab9c19b..fb5f83c2a6bc0 100644 --- a/src/core/server/elasticsearch/client/client_config.test.ts +++ b/src/core/server/elasticsearch/client/client_config.test.ts @@ -16,6 +16,7 @@ const createConfig = ( return { customHeaders: {}, compression: false, + maxSockets: Infinity, sniffOnStart: false, sniffOnConnectionFault: false, sniffInterval: false, @@ -107,6 +108,18 @@ describe('parseClientOptions', () => { }); }); + describe('`maxSockets` option', () => { + it('uses the specified config value', () => { + const options = parseClientOptions(createConfig({ maxSockets: 1024 }), false); + expect(options.agent).toHaveProperty('maxSockets', 1024); + }); + + it('defaults to `Infinity` if not specified by the config', () => { + const options = parseClientOptions(createConfig({}), false); + expect(options.agent).toHaveProperty('maxSockets', Infinity); + }); + }); + describe('`compression` option', () => { it('`compression` is true', () => { const options = parseClientOptions(createConfig({ compression: true }), false); diff --git a/src/core/server/elasticsearch/client/client_config.ts b/src/core/server/elasticsearch/client/client_config.ts index 8a1eacb78d923..9a0b72a36c3db 100644 --- a/src/core/server/elasticsearch/client/client_config.ts +++ b/src/core/server/elasticsearch/client/client_config.ts @@ -22,6 +22,7 @@ import { DEFAULT_HEADERS } from '../default_headers'; export type ElasticsearchClientConfig = Pick< ElasticsearchConfig, | 'customHeaders' + | 'maxSockets' | 'compression' | 'sniffOnStart' | 'sniffOnConnectionFault' @@ -61,7 +62,7 @@ export function parseClientOptions( // fixes https://github.com/elastic/kibana/issues/101944 disablePrototypePoisoningProtection: true, agent: { - maxSockets: Infinity, + maxSockets: config.maxSockets, keepAlive: config.keepAlive ?? true, }, compression: config.compression, diff --git a/src/core/server/elasticsearch/client/cluster_client.test.ts b/src/core/server/elasticsearch/client/cluster_client.test.ts index 98ef7e8dfc4a4..bcec150b4d06e 100644 --- a/src/core/server/elasticsearch/client/cluster_client.test.ts +++ b/src/core/server/elasticsearch/client/cluster_client.test.ts @@ -26,6 +26,7 @@ const createConfig = ( sniffOnStart: false, sniffOnConnectionFault: false, sniffInterval: false, + maxSockets: Infinity, compression: false, requestHeadersWhitelist: ['authorization'], customHeaders: {}, diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index 5c6db10e8695a..47453f1a80610 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -36,6 +36,7 @@ test('set correct defaults', () => { "http://localhost:9200", ], "ignoreVersionMismatch": false, + "maxSockets": Infinity, "password": undefined, "pingTimeout": "PT30S", "requestHeadersWhitelist": Array [ @@ -80,6 +81,45 @@ test('#hosts accepts both string and array of strings', () => { expect(configValue.hosts).toEqual(['http://some.host:1234', 'https://some.another.host']); }); +describe('#maxSockets', () => { + test('accepts positive numeric values', () => { + const configValue = new ElasticsearchConfig(config.schema.validate({ maxSockets: 512 })); + expect(configValue.maxSockets).toEqual(512); + }); + + test('throws if it does not contain a numeric value', () => { + expect(() => { + config.schema.validate({ maxSockets: 'foo' }); + }).toThrowErrorMatchingInlineSnapshot( + `"[maxSockets]: expected value of type [number] but got [string]"` + ); + + expect(() => { + config.schema.validate({ maxSockets: true }); + }).toThrowErrorMatchingInlineSnapshot( + `"[maxSockets]: expected value of type [number] but got [boolean]"` + ); + }); + + test('throws if it does not contain a valid numeric value', () => { + expect(() => { + config.schema.validate({ maxSockets: -1 }); + }).toThrowErrorMatchingInlineSnapshot( + '"[maxSockets]: Value must be equal to or greater than [1]."' + ); + + expect(() => { + config.schema.validate({ maxSockets: 0 }); + }).toThrowErrorMatchingInlineSnapshot( + '"[maxSockets]: Value must be equal to or greater than [1]."' + ); + + expect(() => { + config.schema.validate({ maxSockets: Infinity }); + }).toThrowErrorMatchingInlineSnapshot('"[maxSockets]: \\"maxSockets\\" cannot be infinity"'); + }); +}); + test('#requestHeadersWhitelist accepts both string and array of strings', () => { let configValue = new ElasticsearchConfig( config.schema.validate({ requestHeadersWhitelist: 'token' }) diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 91b1a1f4475e1..1baef038e313a 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -36,6 +36,7 @@ export const configSchema = schema.object({ hosts: schema.oneOf([hostURISchema, schema.arrayOf(hostURISchema, { minSize: 1 })], { defaultValue: 'http://localhost:9200', }), + maxSockets: schema.number({ defaultValue: Infinity, min: 1 }), compression: schema.boolean({ defaultValue: false }), username: schema.maybe( schema.string({ @@ -298,6 +299,11 @@ export class ElasticsearchConfig { */ public readonly apiVersion: string; + /** + * The maximum number of sockets that can be used for communications with elasticsearch. + */ + public readonly maxSockets: number; + /** * Whether to use compression for communications with elasticsearch. */ @@ -405,6 +411,7 @@ export class ElasticsearchConfig { this.password = rawConfig.password; this.serviceAccountToken = rawConfig.serviceAccountToken; this.customHeaders = rawConfig.customHeaders; + this.maxSockets = rawConfig.maxSockets; this.compression = rawConfig.compression; this.skipStartupConnectionCheck = rawConfig.skipStartupConnectionCheck; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7024899388c05..f5516649804a2 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -231,6 +231,7 @@ export const config: { sniffInterval: Type; sniffOnConnectionFault: Type; hosts: Type; + maxSockets: Type; compression: Type; username: Type; password: Type; @@ -889,7 +890,7 @@ export { EcsEventType } export type ElasticsearchClient = Omit; // @public -export type ElasticsearchClientConfig = Pick & { +export type ElasticsearchClientConfig = Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; ssl?: Partial; @@ -907,6 +908,7 @@ export class ElasticsearchConfig { readonly healthCheckDelay: Duration; readonly hosts: string[]; readonly ignoreVersionMismatch: boolean; + readonly maxSockets: number; readonly password?: string; readonly pingTimeout: Duration; readonly requestHeadersWhitelist: string[]; diff --git a/src/plugins/shared_ux/public/components/index.ts b/src/plugins/shared_ux/public/components/index.ts index 82648193e7a92..53577e792aaa2 100644 --- a/src/plugins/shared_ux/public/components/index.ts +++ b/src/plugins/shared_ux/public/components/index.ts @@ -19,9 +19,9 @@ export const LazyExitFullScreenButton = React.lazy(() => })) ); -export const LazySolutionToolbarButton = React.lazy(() => - import('./toolbar/index').then(({ SolutionToolbarButton }) => ({ - default: SolutionToolbarButton, +export const LazyToolbarButton = React.lazy(() => + import('./toolbar/index').then(({ ToolbarButton }) => ({ + default: ToolbarButton, })) ); @@ -35,11 +35,11 @@ export const RedirectAppLinks = React.lazy(() => import('./redirect_app_links')) export const ExitFullScreenButton = withSuspense(LazyExitFullScreenButton); /** - * A `SolutionToolbarButton` component that is wrapped by the `withSuspense` HOC. This component can - * be used directly by consumers and will load the `LazySolutionToolbarButton` component lazily with + * A `ToolbarButton` component that is wrapped by the `withSuspense` HOC. This component can + * be used directly by consumers and will load the `LazyToolbarButton` component lazily with * a predefined fallback and error boundary. */ -export const SolutionToolbarButton = withSuspense(LazySolutionToolbarButton); +export const ToolbarButton = withSuspense(LazyToolbarButton); /** * The Lazily-loaded `NoDataViews` component. Consumers should use `React.Suspennse` or the @@ -57,3 +57,18 @@ export const LazyNoDataViewsPage = React.lazy(() => * a predefined fallback and error boundary. */ export const NoDataViewsPage = withSuspense(LazyNoDataViewsPage); + +/** + * The Lazily-loaded `IconButtonGroup` component. Consumers should use `React.Suspennse` or the + * `withSuspense` HOC to load this component. + */ +export const LazyIconButtonGroup = React.lazy(() => + import('./toolbar/index').then(({ IconButtonGroup }) => ({ + default: IconButtonGroup, + })) +); + +/** + * The IconButtonGroup component that is wrapped by the `withSuspence` HOC. + */ +export const IconButtonGroup = withSuspense(LazyIconButtonGroup); diff --git a/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/__snapshots__/icon_button_group.test.tsx.snap b/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/__snapshots__/icon_button_group.test.tsx.snap new file mode 100644 index 0000000000000..9e0e13d628702 --- /dev/null +++ b/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/__snapshots__/icon_button_group.test.tsx.snap @@ -0,0 +1,216 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` is rendered 1`] = ` +.emotion-0.euiButtonGroupButton { + background-color: #FFF; + border: 1px solid #D3DAE6!important; +} + +.emotion-0.euiButtonGroupButton:first-of-type { + border-top-left-radius: 6px!important; + border-bottom-left-radius: 6px!important; +} + +.emotion-0.euiButtonGroupButton:last-of-type { + border-top-right-radius: 6px!important; + border-bottom-right-radius: 6px!important; +} + + + + +
+ + + Legend + + +
+ + + + + +
+
+
+
+
+`; diff --git a/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/icon_button_group.mdx b/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/icon_button_group.mdx new file mode 100644 index 0000000000000..979bfb24a0f2f --- /dev/null +++ b/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/icon_button_group.mdx @@ -0,0 +1,20 @@ +--- +id: sharedUX/Components/Toolbar/Icon_Button_Group +slug: /shared-ux/components/toolbar/icon_button_group +title: Toolbar Icon Button Group +summary: 'An array of icon-only buttons for use in a toolbar' +tags: ['shared-ux', 'component'] +date: 2022-02-23 +--- + +> This documentation is in-progress. + +This component requires a prop that consists of a series of buttons that can then be displayed based on the number of buttons desired. An example of a button that can be part of an array of icon buttons is included below: + +``` + { + label: 'Text', + onClick: clickHandler, + iconType: 'visText', + } +``` diff --git a/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/icon_button_group.stories.tsx b/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/icon_button_group.stories.tsx new file mode 100644 index 0000000000000..c30f015325672 --- /dev/null +++ b/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/icon_button_group.stories.tsx @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { action } from '@storybook/addon-actions'; +import { Story } from '@storybook/react'; +import React from 'react'; +import { IconButtonGroup } from './icon_button_group'; +import mdx from './icon_button_group.mdx'; + +export default { + title: 'Toolbar/Icon Button Group', + description: 'A collection of buttons that is a part of a toolbar.', + parameters: { + docs: { + page: mdx, + }, + }, +}; + +const quickButtons = [ + { + label: 'Text', + onClick: action('onTextClick'), + iconType: 'visText', + title: 'Text as markdown', + }, + { + label: 'Control', + onClick: action('onControlClick'), + iconType: 'controlsHorizontal', + }, + { + label: 'Link', + onClick: action('onLinkClick'), + iconType: 'link', + }, + { + label: 'Image', + onClick: action('onImageClick'), + iconType: 'image', + }, + { + label: 'Markup', + onClick: action('onMarkupClick'), + iconType: 'visVega', + }, +]; + +export const ConnectedComponent: Story<{ buttonCount: number }> = ({ buttonCount }) => { + return ( + + ); +}; + +ConnectedComponent.argTypes = { + buttonCount: { + defaultValue: 2, + control: { + type: 'number', + min: 1, + max: 5, + step: 1, + }, + }, +}; diff --git a/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/icon_button_group.styles.ts b/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/icon_button_group.styles.ts new file mode 100644 index 0000000000000..4723cca957082 --- /dev/null +++ b/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/icon_button_group.styles.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UseEuiTheme } from '@elastic/eui'; + +export const IconButtonGroupStyles = ({ euiTheme }: UseEuiTheme) => { + return { + button: { + '&.euiButtonGroupButton': { + backgroundColor: euiTheme.colors.emptyShade, + border: `${euiTheme.border.thin} !important`, + '&:first-of-type': { + borderTopLeftRadius: `${euiTheme.border.radius.medium} !important`, + borderBottomLeftRadius: `${euiTheme.border.radius.medium} !important`, + }, + '&:last-of-type': { + borderTopRightRadius: `${euiTheme.border.radius.medium} !important`, + borderBottomRightRadius: `${euiTheme.border.radius.medium} !important`, + }, + }, + }, + }; +}; diff --git a/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/icon_button_group.test.tsx b/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/icon_button_group.test.tsx new file mode 100644 index 0000000000000..751476bb5648c --- /dev/null +++ b/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/icon_button_group.test.tsx @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mount as enzymeMount, ReactWrapper } from 'enzyme'; + +import { ServicesProvider, SharedUXServices } from '../../../../services'; +import { servicesFactory } from '../../../../services/mocks'; +import { IconButtonGroup } from './icon_button_group'; + +describe('', () => { + let services: SharedUXServices; + let mount: (element: JSX.Element) => ReactWrapper; + + beforeEach(() => { + services = servicesFactory(); + mount = (element: JSX.Element) => + enzymeMount({element}); + }); + + test('is rendered', () => { + const component = mount( + + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/icon_button_group.tsx b/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/icon_button_group.tsx new file mode 100644 index 0000000000000..3ac5f87622d99 --- /dev/null +++ b/src/plugins/shared_ux/public/components/toolbar/buttons/icon_button_group/icon_button_group.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiButtonGroup, + htmlIdGenerator, + EuiButtonGroupOptionProps, + useEuiTheme, + IconType, + EuiButtonGroupProps, +} from '@elastic/eui'; +import { IconButtonGroupStyles } from './icon_button_group.styles'; +export interface IconButton { + /** + * The accessible button label + */ + label: string; + /** + * EUI `IconType` to display + */ + iconType: IconType; + onClick: () => void; + /** + * HTML `title` attribute for tooltips if different from `label` + */ + title?: string; +} + +export interface Props { + /** + * Required accessible legend for the whole group + */ + legend: EuiButtonGroupProps['legend']; + /** + * Array of `QuickButton`s + */ + buttons: IconButton[]; +} + +type Option = EuiButtonGroupOptionProps & Omit; + +export const IconButtonGroup = ({ buttons, legend }: Props) => { + const euiTheme = useEuiTheme(); + const iconButtonGroupStyles = IconButtonGroupStyles(euiTheme); + + const buttonGroupOptions: Option[] = buttons.map((button: IconButton, index) => { + const { label, title = label, ...rest } = button; + + return { + ...rest, + 'aria-label': title ?? label, + id: `${htmlIdGenerator()()}${index}`, + label, + title, + css: [iconButtonGroupStyles.button], + }; + }); + + const onChangeIconsMulti = (optionId: string) => { + buttonGroupOptions.find((x) => x.id === optionId)?.onClick(); + }; + + return ( + + ); +}; diff --git a/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/__snapshots__/primary.test.tsx.snap b/src/plugins/shared_ux/public/components/toolbar/buttons/primary/__snapshots__/primary.test.tsx.snap similarity index 94% rename from src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/__snapshots__/primary.test.tsx.snap rename to src/plugins/shared_ux/public/components/toolbar/buttons/primary/__snapshots__/primary.test.tsx.snap index 8148afa76d790..eecbcee5d99eb 100644 --- a/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/__snapshots__/primary.test.tsx.snap +++ b/src/plugins/shared_ux/public/components/toolbar/buttons/primary/__snapshots__/primary.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` is rendered 1`] = ` +exports[` is rendered 1`] = ` is rendered 1`] = ` } } > - is rendered 1`] = ` - + `; diff --git a/src/plugins/shared_ux/public/components/toolbar/buttons/primary/primary.mdx b/src/plugins/shared_ux/public/components/toolbar/buttons/primary/primary.mdx new file mode 100644 index 0000000000000..489596e771c29 --- /dev/null +++ b/src/plugins/shared_ux/public/components/toolbar/buttons/primary/primary.mdx @@ -0,0 +1,12 @@ +--- +id: sharedUX/Components/Toolbar/Primary_Button +slug: /shared-ux/components/toolbar/buttons/primary +title: Toolbar Button +summary: An opinionated implementation of the toolbar extracted to just the button. +tags: ['shared-ux', 'component'] +date: 2022-02-17 +--- + +> This documentation is in-progress. + +This button is a part of the toolbar component. This button has primary styling and requires a `label`. Interaction (`onClick`) handlers and `iconType`s are supported as an extension of EuiButtonProps. Icons are always on the left of any labels within the button. diff --git a/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.stories.tsx b/src/plugins/shared_ux/public/components/toolbar/buttons/primary/primary.stories.tsx similarity index 78% rename from src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.stories.tsx rename to src/plugins/shared_ux/public/components/toolbar/buttons/primary/primary.stories.tsx index 56c15ec7749af..0388cccb60c3f 100644 --- a/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.stories.tsx +++ b/src/plugins/shared_ux/public/components/toolbar/buttons/primary/primary.stories.tsx @@ -8,12 +8,12 @@ import { Story } from '@storybook/react'; import React from 'react'; -import { SolutionToolbarButton } from './primary'; +import { ToolbarButton } from './primary'; import mdx from './primary.mdx'; export default { - title: 'Solution Toolbar Button', - description: 'A button that is a part of the solution toolbar.', + title: 'Toolbar/Primary button', + description: 'A primary button that is a part of a toolbar.', parameters: { docs: { page: mdx, @@ -33,7 +33,7 @@ export default { export const Component: Story<{ iconType: any; }> = ({ iconType }) => { - return ; + return ; }; Component.args = { diff --git a/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.test.tsx b/src/plugins/shared_ux/public/components/toolbar/buttons/primary/primary.test.tsx similarity index 81% rename from src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.test.tsx rename to src/plugins/shared_ux/public/components/toolbar/buttons/primary/primary.test.tsx index c2e5fd1ce7ab8..adcf9e178feaa 100644 --- a/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.test.tsx +++ b/src/plugins/shared_ux/public/components/toolbar/buttons/primary/primary.test.tsx @@ -11,9 +11,9 @@ import React from 'react'; import { ServicesProvider, SharedUXServices } from '../../../../services'; import { servicesFactory } from '../../../../services/mocks'; -import { SolutionToolbarButton } from './primary'; +import { ToolbarButton } from './primary'; -describe('', () => { +describe('', () => { let services: SharedUXServices; let mount: (element: JSX.Element) => ReactWrapper; @@ -28,13 +28,13 @@ describe('', () => { }); test('is rendered', () => { - const component = mount(); + const component = mount(); expect(component).toMatchSnapshot(); }); test('it can be passed a functional onClick handler', () => { const mockHandler = jest.fn(); - const component = mount(); + const component = mount(); component.simulate('click'); expect(mockHandler).toHaveBeenCalled(); }); diff --git a/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.tsx b/src/plugins/shared_ux/public/components/toolbar/buttons/primary/primary.tsx similarity index 91% rename from src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.tsx rename to src/plugins/shared_ux/public/components/toolbar/buttons/primary/primary.tsx index b99af852ed7e3..48677d965fa6c 100644 --- a/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.tsx +++ b/src/plugins/shared_ux/public/components/toolbar/buttons/primary/primary.tsx @@ -14,7 +14,7 @@ export interface Props extends Pick { +export const ToolbarButton = ({ label, ...rest }: Props) => { return ( {label} diff --git a/src/plugins/shared_ux/public/components/toolbar/index.ts b/src/plugins/shared_ux/public/components/toolbar/index.ts index de15e73eaadeb..e68abf2916a72 100644 --- a/src/plugins/shared_ux/public/components/toolbar/index.ts +++ b/src/plugins/shared_ux/public/components/toolbar/index.ts @@ -6,4 +6,5 @@ * Side Public License, v 1. */ -export { SolutionToolbarButton } from './solution_toolbar/button/primary'; +export { ToolbarButton } from './buttons/primary/primary'; +export { IconButtonGroup } from './buttons/icon_button_group/icon_button_group'; diff --git a/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.mdx b/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.mdx deleted file mode 100644 index 6693277b370ae..0000000000000 --- a/src/plugins/shared_ux/public/components/toolbar/solution_toolbar/button/primary.mdx +++ /dev/null @@ -1,12 +0,0 @@ ---- -id: sharedUX/Components/SolutionToolbarButton -slug: /shared-ux/components/toolbar/solution_toolbar/button/primary -title: Solution Toolbar Button -summary: An opinionated implementation of the toolbar extracted to just the button. -tags: ['shared-ux', 'component'] -date: 2022-02-17 ---- - -> This documentation is in-progress. - -This button is a part of the solution toolbar component. This button has primary styling and requires a label. OnClick handlers and icon types are supported as an extension of EuiButtonProps. Icons are always on the left of any labels within the button. diff --git a/src/plugins/shared_ux/public/index.ts b/src/plugins/shared_ux/public/index.ts index a196a60db847b..5505f5ffb1e02 100755 --- a/src/plugins/shared_ux/public/index.ts +++ b/src/plugins/shared_ux/public/index.ts @@ -18,3 +18,4 @@ export function plugin() { export type { SharedUXPluginSetup, SharedUXPluginStart } from './types'; export { ExitFullScreenButton, LazyExitFullScreenButton } from './components'; export { NoDataViewsPage, LazyNoDataViewsPage } from './components'; +export { IconButtonGroup } from './components'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx index a1f044241e5d3..980e85f66e3f5 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx @@ -44,6 +44,7 @@ const ScoreChart = ({ return ( ( - + { const tableRef = useRef(null); const [changedRules, setChangedRules] = useState>(new Map()); + const [selectedRuleId, setSelectedRuleId] = useState(null); const [isAllSelected, setIsAllSelected] = useState(false); const [visibleSelectedRulesIds, setVisibleSelectedRulesIds] = useState([]); const [rulesQuery, setRulesQuery] = useState({ page: 0, perPage: 5, search: '' }); @@ -178,11 +180,19 @@ export const RulesContainer = () => { setPagination={(paginationQuery) => setRulesQuery((currentQuery) => ({ ...currentQuery, ...paginationQuery })) } + setSelectedRuleId={setSelectedRuleId} + selectedRuleId={selectedRuleId} /> {hasChanges && ( )} + {selectedRuleId && ( + setSelectedRuleId(null)} + /> + )} ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx new file mode 100644 index 0000000000000..8e8e0e37eed0d --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiSpacer, EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody } from '@elastic/eui'; +import type { RuleSavedObject } from './use_csp_rules'; + +interface FindingFlyoutProps { + onClose(): void; + rule: RuleSavedObject; +} + +export const RuleFlyout = ({ onClose, rule }: FindingFlyoutProps) => { + return ( + + +

{rule.attributes.name}

+ +
+ +
+ ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx index e91954a381591..f9fbe1ab4ae8e 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx @@ -7,13 +7,16 @@ import React, { useMemo } from 'react'; import { Criteria, - EuiLink, + EuiButtonEmpty, EuiSwitch, + EuiToolTip, EuiTableFieldDataColumnType, EuiBasicTable, EuiBasicTableProps, + useEuiTheme, } from '@elastic/eui'; import moment from 'moment'; +import { FormattedMessage } from '@kbn/i18n-react'; import type { RulesState } from './rules_container'; import * as TEST_SUBJECTS from './test_subjects'; import * as TEXT from './translations'; @@ -26,6 +29,8 @@ type RulesTableProps = Pick< toggleRule(rule: RuleSavedObject): void; setSelectedRules(rules: RuleSavedObject[]): void; setPagination(pagination: Pick): void; + setSelectedRuleId(id: string | null): void; + selectedRuleId: string | null; // ForwardRef makes this ref not available in parent callbacks tableRef: React.RefObject>; }; @@ -34,6 +39,7 @@ export const RulesTable = ({ toggleRule, setSelectedRules, setPagination, + setSelectedRuleId, perPage: pageSize, rules_page: items, page, @@ -41,8 +47,13 @@ export const RulesTable = ({ total, loading, error, + selectedRuleId, }: RulesTableProps) => { - const columns = useMemo(() => getColumns({ toggleRule }), [toggleRule]); + const { euiTheme } = useEuiTheme(); + const columns = useMemo( + () => getColumns({ toggleRule, setSelectedRuleId }), + [setSelectedRuleId, toggleRule] + ); const euiPagination: EuiBasicTableProps['pagination'] = { pageIndex: page, @@ -62,6 +73,16 @@ export const RulesTable = ({ setPagination({ page: pagination.index, perPage: pagination.size }); }; + const rowProps = (row: RuleSavedObject) => ({ + style: { background: row.id === selectedRuleId ? euiTheme.colors.highlight : undefined }, + onClick: (e: MouseEvent) => { + const tag = (e.target as HTMLDivElement).tagName; + // Ignore checkbox and switch toggle columns + if (tag === 'BUTTON' || tag === 'INPUT') return; + setSelectedRuleId(row.id); + }, + }); + return ( v.id} + rowProps={rowProps} /> ); }; -const ruleNameRenderer = (name: string) => ( - - {name} - -); - -const timestampRenderer = (timestamp: string) => - moment.duration(moment().diff(timestamp)).humanize(); - -interface GetColumnProps { +interface GetColumnProps extends Pick { toggleRule: (rule: RuleSavedObject) => void; } -const createRuleEnabledSwitchRenderer = - ({ toggleRule }: GetColumnProps) => - (value: boolean, rule: RuleSavedObject) => - ( - toggleRule(rule)} - data-test-subj={TEST_SUBJECTS.getCspRulesTableItemSwitchTestId(rule.attributes.id)} - /> - ); - const getColumns = ({ toggleRule, + setSelectedRuleId, }: GetColumnProps): Array> => [ { field: 'attributes.name', name: TEXT.RULE_NAME, width: '60%', truncateText: true, - render: ruleNameRenderer, + render: (name, rule) => ( + ) => { + e.stopPropagation(); + setSelectedRuleId(rule.id); + }} + > + {name} + + ), }, { field: 'section', // TODO: what field is this? @@ -124,12 +136,36 @@ const getColumns = ({ field: 'updatedAt', name: TEXT.UPDATED_AT, width: '15%', - render: timestampRenderer, + render: (timestamp) => moment(timestamp).fromNow(), }, { field: 'attributes.enabled', name: TEXT.ENABLED, - render: createRuleEnabledSwitchRenderer({ toggleRule }), + render: (enabled, rule) => ( + + ) : ( + + ) + } + > + toggleRule(rule)} + data-test-subj={TEST_SUBJECTS.getCspRulesTableItemSwitchTestId(rule.attributes.id)} + /> + + ), width: '10%', }, ]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx index bf2a122ead42b..b0519d16bab10 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx @@ -5,17 +5,7 @@ * 2.0. */ -import React from 'react'; - -import { EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import { META_ENGINES_DOCS_URL } from '../../routes'; -import { - META_ENGINE_CREATION_FORM_META_ENGINE_DESCRIPTION, - META_ENGINE_CREATION_FORM_DOCUMENTATION_LINK, -} from '../meta_engine_creation/constants'; export const ENGINES_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.engines.title', { defaultMessage: 'Engines', @@ -31,24 +21,6 @@ export const META_ENGINES_TITLE = i18n.translate( { defaultMessage: 'Meta Engines' } ); -export const META_ENGINES_DESCRIPTION = ( - <> - {META_ENGINE_CREATION_FORM_META_ENGINE_DESCRIPTION} -
- - {META_ENGINE_CREATION_FORM_DOCUMENTATION_LINK} - - ), - }} - /> - -); - export const SOURCE_ENGINES_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.enginesOverview.metaEnginesTable.sourceEngines.title', { defaultMessage: 'Source Engines' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx index 62cf0fd0f6edf..90fc47a8d493a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx @@ -156,19 +156,6 @@ describe('EnginesOverview', () => { }); }); - describe('when an account does not have a platinum license', () => { - it('renders a license call to action in place of the meta engines table', () => { - setMockValues({ - ...valuesWithEngines, - hasPlatinumLicense: false, - }); - const wrapper = shallow(); - - expect(wrapper.find('[data-test-subj="metaEnginesLicenseCTA"]')).toHaveLength(1); - expect(wrapper.find('[data-test-subj="appSearchMetaEngines"]')).toHaveLength(0); - }); - }); - describe('pagination', () => { const getTablePagination = (wrapper: ShallowWrapper) => wrapper.find(EnginesTable).prop('pagination'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index 07ac7ae059560..cb4bb7d40f47a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -11,7 +11,6 @@ import { useValues, useActions } from 'kea'; import { EuiSpacer } from '@elastic/eui'; -import { LicensingLogic, ManageLicenseButton } from '../../../shared/licensing'; import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { convertMetaToPagination, handlePageChange } from '../../../shared/table_pagination'; import { AppLogic } from '../../app_logic'; @@ -30,12 +29,10 @@ import { CREATE_A_META_ENGINE_BUTTON_LABEL, ENGINES_TITLE, META_ENGINES_TITLE, - META_ENGINES_DESCRIPTION, } from './constants'; import { EnginesLogic } from './engines_logic'; export const EnginesOverview: React.FC = () => { - const { hasPlatinumLicense } = useValues(LicensingLogic); const { myRole: { canManageEngines, canManageMetaEngines }, } = useValues(AppLogic); @@ -58,8 +55,8 @@ export const EnginesOverview: React.FC = () => { }, [enginesMeta.page.current]); useEffect(() => { - if (hasPlatinumLicense) loadMetaEngines(); - }, [hasPlatinumLicense, metaEnginesMeta.page.current]); + loadMetaEngines(); + }, [metaEnginesMeta.page.current]); return ( { /> - {hasPlatinumLicense ? ( - {META_ENGINES_TITLE}} - titleSize="s" - action={ - canManageMetaEngines && ( - - {CREATE_A_META_ENGINE_BUTTON_LABEL} - - ) - } - data-test-subj="appSearchMetaEngines" - > - } - onChange={handlePageChange(onMetaEnginesPagination)} - /> - - ) : ( - {META_ENGINES_TITLE}} - titleSize="s" - subtitle={META_ENGINES_DESCRIPTION} - action={} - data-test-subj="metaEnginesLicenseCTA" + {META_ENGINES_TITLE}} + titleSize="s" + action={ + canManageMetaEngines && ( + + {CREATE_A_META_ENGINE_BUTTON_LABEL} + + ) + } + data-test-subj="appSearchMetaEngines" + > + } + onChange={handlePageChange(onMetaEnginesPagination)} /> - )} + ); diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index 87f75b86da485..b355a62fbf241 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -2606,6 +2606,9 @@ }, "diff": { "$ref": "#/components/schemas/upgrade_diff" + }, + "agent_diff": { + "$ref": "#/components/schemas/upgrade_agent_diff" } }, "required": [ @@ -4090,9 +4093,12 @@ "type": "string" }, "last_activity_ms": { - "type": "string" + "type": "number" }, "size_in_bytes": { + "type": "number" + }, + "size_in_bytes_formatted": { "type": "string" }, "dashboard": { @@ -4213,6 +4219,118 @@ ] } }, + "full_agent_policy_input_stream": { + "title": "Full agent policy input stream", + "allOf": [ + { + "type": "object", + "additionalProperties": true, + "properties": { + "id": { + "type": "string" + }, + "data_stream": { + "type": "object", + "properties": { + "dataset": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "dataset", + "type" + ] + } + }, + "required": [ + "id", + "data_stream" + ] + } + ] + }, + "full_agent_policy_input": { + "title": "Full agent policy input", + "allOf": [ + { + "type": "object", + "additionalProperties": true, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "revision": { + "type": "number" + }, + "type": { + "type": "string" + }, + "data_stream": { + "type": "object", + "properties": { + "namespace": { + "type": "string" + } + }, + "required": [ + "namespace" + ] + }, + "use_output": { + "type": "string" + }, + "meta": { + "type": "object", + "additionalProperties": true, + "properties": { + "package": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "required": [ + "name", + "version" + ] + } + } + }, + "streams": { + "$ref": "#/components/schemas/full_agent_policy_input_stream" + } + }, + "required": [ + "id", + "name", + "revision", + "type", + "data_stream", + "use_output" + ] + } + ] + }, + "upgrade_agent_diff": { + "title": "Package policy Upgrade dryrun", + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/full_agent_policy_input" + } + } + }, "update_package_policy": { "title": "Update package policy", "type": "object", diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index dd5569779ee19..9a352f94e8252 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -1599,6 +1599,8 @@ paths: type: boolean diff: $ref: '#/components/schemas/upgrade_diff' + agent_diff: + $ref: '#/components/schemas/upgrade_agent_diff' required: - hasErrors /package_policies/{packagePolicyId}: @@ -2572,8 +2574,10 @@ components: package_version: type: string last_activity_ms: - type: string + type: number size_in_bytes: + type: number + size_in_bytes_formatted: type: string dashboard: type: array @@ -2648,6 +2652,80 @@ components: type: array items: type: string + full_agent_policy_input_stream: + title: Full agent policy input stream + allOf: + - type: object + additionalProperties: true + properties: + id: + type: string + data_stream: + type: object + properties: + dataset: + type: string + type: + type: string + required: + - dataset + - type + required: + - id + - data_stream + full_agent_policy_input: + title: Full agent policy input + allOf: + - type: object + additionalProperties: true + properties: + id: + type: string + name: + type: string + revision: + type: number + type: + type: string + data_stream: + type: object + properties: + namespace: + type: string + required: + - namespace + use_output: + type: string + meta: + type: object + additionalProperties: true + properties: + package: + type: object + properties: + name: + type: string + version: + type: string + required: + - name + - version + streams: + $ref: '#/components/schemas/full_agent_policy_input_stream' + required: + - id + - name + - revision + - type + - data_stream + - use_output + upgrade_agent_diff: + title: Package policy Upgrade dryrun + type: array + items: + type: array + items: + $ref: '#/components/schemas/full_agent_policy_input' update_package_policy: title: Update package policy type: object diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/full_agent_policy_input.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/full_agent_policy_input.yaml new file mode 100644 index 0000000000000..6fc95923bef22 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/full_agent_policy_input.yaml @@ -0,0 +1,45 @@ +title: Full agent policy input +allOf: + - type: object + additionalProperties: true + properties: + id: + type: string + name: + type: string + revision: + type: number + type: + type: string + data_stream: + type: object + properties: + namespace: + type: string + required: + - namespace + use_output: + type: string + meta: + type: object + additionalProperties: true + properties: + package: + type: object + properties: + name: + type: string + version: + type: string + required: + - name + - version + streams: + $ref: ./full_agent_policy_input_stream.yaml + required: + - id + - name + - revision + - type + - data_stream + - use_output diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/full_agent_policy_input_stream.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/full_agent_policy_input_stream.yaml new file mode 100644 index 0000000000000..f0d51bbe03cac --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/full_agent_policy_input_stream.yaml @@ -0,0 +1,20 @@ +title: Full agent policy input stream +allOf: + - type: object + additionalProperties: true + properties: + id: + type: string + data_stream: + type: object + properties: + dataset: + type: string + type: + type: string + required: + - dataset + - type + required: + - id + - data_stream diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/upgrade_agent_diff.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/upgrade_agent_diff.yaml new file mode 100644 index 0000000000000..efed1601137e3 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/upgrade_agent_diff.yaml @@ -0,0 +1,6 @@ +title: Package policy Upgrade dryrun +type: array +items: + type: array + items: + $ref: ./full_agent_policy_input.yaml \ No newline at end of file diff --git a/x-pack/plugins/fleet/common/openapi/paths/package_policies@upgrade_dryrun.yaml b/x-pack/plugins/fleet/common/openapi/paths/package_policies@upgrade_dryrun.yaml index c7d2269f917b8..ec66a754bfcfc 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/package_policies@upgrade_dryrun.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/package_policies@upgrade_dryrun.yaml @@ -29,5 +29,7 @@ post: type: boolean diff: $ref: ../components/schemas/upgrade_diff.yaml + agent_diff: + $ref: ../components/schemas/upgrade_agent_diff.yaml required: - hasErrors diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts index 472047f6f496e..98f047ae23462 100644 --- a/x-pack/plugins/fleet/common/services/index.ts +++ b/x-pack/plugins/fleet/common/services/index.ts @@ -12,7 +12,6 @@ export { packageToPackagePolicy, getStreamsForInputType, } from './package_to_package_policy'; -export { storedPackagePoliciesToAgentInputs } from './package_policies_to_agent_inputs'; export { fullAgentPolicyToYaml } from './full_agent_policy_to_yaml'; export { isPackageLimited, doesAgentPolicyAlreadyIncludePackage } from './limited_package'; export { decodeCloudId } from './decode_cloud_id'; diff --git a/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.ts b/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.ts deleted file mode 100644 index b9b2c70815fa5..0000000000000 --- a/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { merge } from 'lodash'; - -import type { PackagePolicy, FullAgentPolicyInput, FullAgentPolicyInputStream } from '../types'; -import { DEFAULT_OUTPUT } from '../constants'; - -export const storedPackagePoliciesToAgentInputs = ( - packagePolicies: PackagePolicy[], - outputId: string = DEFAULT_OUTPUT.name -): FullAgentPolicyInput[] => { - const fullInputs: FullAgentPolicyInput[] = []; - - packagePolicies.forEach((packagePolicy) => { - if (!packagePolicy.enabled || !packagePolicy.inputs || !packagePolicy.inputs.length) { - return; - } - packagePolicy.inputs.forEach((input) => { - if (!input.enabled) { - return; - } - - const fullInput: FullAgentPolicyInput = { - id: `${input.type}${input.policy_template ? `-${input.policy_template}-` : '-'}${ - packagePolicy.id - }`, - revision: packagePolicy.revision, - name: packagePolicy.name, - type: input.type, - data_stream: { - namespace: packagePolicy.namespace || 'default', - }, - use_output: outputId, - ...(input.compiled_input || {}), - ...(input.streams.length - ? { - streams: input.streams - .filter((stream) => stream.enabled) - .map((stream) => { - const fullStream: FullAgentPolicyInputStream = { - id: stream.id, - data_stream: stream.data_stream, - ...stream.compiled_stream, - ...Object.entries(stream.config || {}).reduce((acc, [key, { value }]) => { - acc[key] = value; - return acc; - }, {} as { [k: string]: any }), - }; - return fullStream; - }), - } - : {}), - }; - - // deeply merge the input.config values with the full policy input - merge( - fullInput, - Object.entries(input.config || {}).reduce( - (acc, [key, { value }]) => ({ ...acc, [key]: value }), - {} - ) - ); - - if (packagePolicy.package) { - fullInput.meta = { - package: { - name: packagePolicy.package.name, - version: packagePolicy.package.version, - }, - }; - } - - fullInputs.push(fullInput); - }); - }); - - return fullInputs; -}; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts b/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts index 740f0401dca6e..35202a0eb42aa 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts @@ -11,6 +11,7 @@ import type { UpdatePackagePolicy, DryRunPackagePolicy, PackagePolicyPackage, + FullAgentPolicyInput, } from '../models'; import type { ListResult, ListWithKuery } from './common'; @@ -72,6 +73,7 @@ export interface UpgradePackagePolicyBaseResponse { export interface UpgradePackagePolicyDryRunResponseItem extends UpgradePackagePolicyBaseResponse { hasErrors: boolean; diff?: [PackagePolicy, DryRunPackagePolicy]; + agent_diff?: [FullAgentPolicyInput[]]; } export type UpgradePackagePolicyDryRunResponse = UpgradePackagePolicyDryRunResponseItem[]; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 8a8e8f5266fe5..c606fbd91f6c8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -63,7 +63,7 @@ import type { UpgradePackagePolicyDryRunResponse, } from '../../../../../../common/types/rest_spec'; import type { PackagePolicyEditExtensionComponentProps } from '../../../types'; -import { pkgKeyFromPackageInfo, storedPackagePoliciesToAgentInputs } from '../../../services'; +import { pkgKeyFromPackageInfo } from '../../../services'; import { EuiButtonWithTooltip } from '../../../../integrations/sections/epm/screens/detail'; import { fixApmDurationVars, hasUpgradeAvailable } from './utils'; @@ -784,11 +784,7 @@ const UpgradeStatusCallout: React.FunctionComponent<{ - {JSON.stringify( - storedPackagePoliciesToAgentInputs([currentPackagePolicy]), - null, - 2 - )} + {JSON.stringify(dryRunData[0].agent_diff?.[0] || [], null, 2)} diff --git a/x-pack/plugins/fleet/public/services/index.ts b/x-pack/plugins/fleet/public/services/index.ts index ad108e3f97ca9..306b081dce6c4 100644 --- a/x-pack/plugins/fleet/public/services/index.ts +++ b/x-pack/plugins/fleet/public/services/index.ts @@ -27,7 +27,6 @@ export { appRoutesService, packageToPackagePolicy, packageToPackagePolicyInputs, - storedPackagePoliciesToAgentInputs, fullAgentPolicyToYaml, isPackageLimited, doesAgentPolicyAlreadyIncludePackage, diff --git a/x-pack/plugins/fleet/server/integration_tests/__snapshots__/cloud_preconfiguration.test.ts.snap b/x-pack/plugins/fleet/server/integration_tests/__snapshots__/cloud_preconfiguration.test.ts.snap index 88b4a2161a0ba..9212aa32a1c60 100644 --- a/x-pack/plugins/fleet/server/integration_tests/__snapshots__/cloud_preconfiguration.test.ts.snap +++ b/x-pack/plugins/fleet/server/integration_tests/__snapshots__/cloud_preconfiguration.test.ts.snap @@ -111,7 +111,7 @@ Object { "data_stream": Object { "namespace": "default", }, - "id": "apm-apmserver-elastic-cloud-apm", + "id": "elastic-cloud-apm", "meta": Object { "package": Object { "name": "apm", diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts index 5d11c91889e25..0bc5315f62ddf 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts @@ -21,12 +21,13 @@ import { storedPackagePoliciesToAgentPermissions, DEFAULT_CLUSTER_PERMISSIONS, } from '../package_policies_to_agent_permissions'; -import { storedPackagePoliciesToAgentInputs, dataTypes, outputType } from '../../../common'; +import { dataTypes, outputType } from '../../../common'; import type { FullAgentPolicyOutputPermissions } from '../../../common'; import { getSettings } from '../settings'; import { DEFAULT_OUTPUT } from '../../constants'; import { getMonitoringPermissions } from './monitoring_permissions'; +import { storedPackagePoliciesToAgentInputs } from './'; export async function getFullAgentPolicy( soClient: SavedObjectsClientContract, @@ -86,7 +87,8 @@ export async function getFullAgentPolicy( return acc; }, {}), }, - inputs: storedPackagePoliciesToAgentInputs( + inputs: await storedPackagePoliciesToAgentInputs( + soClient, agentPolicy.package_policies as PackagePolicy[], getOutputIdForAgentPolicy(dataOutput) ), diff --git a/x-pack/plugins/fleet/server/services/agent_policies/index.ts b/x-pack/plugins/fleet/server/services/agent_policies/index.ts index 2e1fffdec1147..92cf5b90ca3f6 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/index.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/index.ts @@ -6,4 +6,8 @@ */ export { getFullAgentPolicy } from './full_agent_policy'; +export { + storedPackagePolicyToAgentInputs, + storedPackagePoliciesToAgentInputs, +} from './package_policies_to_agent_inputs'; export { validateOutputForPolicy } from './validate_outputs_for_policy'; diff --git a/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_inputs.test.ts similarity index 78% rename from x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.test.ts rename to x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_inputs.test.ts index 3b1ea6dfd4227..0812e3f593c66 100644 --- a/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_inputs.test.ts @@ -4,11 +4,42 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { savedObjectsClientMock } from 'src/core/server/mocks'; -import type { PackagePolicy, PackagePolicyInput } from '../types'; +import type { PackagePolicy, PackagePolicyInput } from '../../types'; import { storedPackagePoliciesToAgentInputs } from './package_policies_to_agent_inputs'; +async function mockedGetPackageInfo({ pkgName }: { pkgName: string }) { + const packages: Record = { + 'mock-package': { + name: 'mock-package', + version: '0.0.0', + policy_templates: [ + { + multiple: true, + }, + ], + }, + 'limited-package': { + name: 'limited-package', + version: '0.0.0', + policy_templates: [ + { + multiple: false, + }, + ], + }, + }; + return Promise.resolve(packages[pkgName]); +} + +jest.mock('../epm/packages', () => { + return { + getPackageInfo: jest.fn().mockImplementation(mockedGetPackageInfo), + }; +}); + describe('Fleet - storedPackagePoliciesToAgentInputs', () => { const mockPackagePolicy: PackagePolicy = { id: 'some-uuid', @@ -107,11 +138,13 @@ describe('Fleet - storedPackagePoliciesToAgentInputs', () => { ], }; - it('returns no inputs for package policy with no inputs, or only disabled inputs', () => { - expect(storedPackagePoliciesToAgentInputs([mockPackagePolicy])).toEqual([]); + it('returns no inputs for package policy with no inputs, or only disabled inputs', async () => { + expect( + await storedPackagePoliciesToAgentInputs(savedObjectsClientMock.create(), [mockPackagePolicy]) + ).toEqual([]); expect( - storedPackagePoliciesToAgentInputs([ + await storedPackagePoliciesToAgentInputs(savedObjectsClientMock.create(), [ { ...mockPackagePolicy, package: { @@ -124,7 +157,7 @@ describe('Fleet - storedPackagePoliciesToAgentInputs', () => { ).toEqual([]); expect( - storedPackagePoliciesToAgentInputs([ + await storedPackagePoliciesToAgentInputs(savedObjectsClientMock.create(), [ { ...mockPackagePolicy, inputs: [{ ...mockInput, enabled: false }], @@ -133,9 +166,9 @@ describe('Fleet - storedPackagePoliciesToAgentInputs', () => { ).toEqual([]); }); - it('returns agent inputs with streams', () => { + it('returns agent inputs with streams', async () => { expect( - storedPackagePoliciesToAgentInputs([ + await storedPackagePoliciesToAgentInputs(savedObjectsClientMock.create(), [ { ...mockPackagePolicy, package: { @@ -176,9 +209,9 @@ describe('Fleet - storedPackagePoliciesToAgentInputs', () => { ]); }); - it('returns unique agent inputs IDs, with policy template name if one exists', () => { + it('returns unique agent inputs IDs, with policy template name if one exists for non-limited packages', async () => { expect( - storedPackagePoliciesToAgentInputs([ + await storedPackagePoliciesToAgentInputs(savedObjectsClientMock.create(), [ { ...mockPackagePolicy, package: { @@ -188,6 +221,15 @@ describe('Fleet - storedPackagePoliciesToAgentInputs', () => { }, inputs: [mockInput, mockInput2], }, + { + ...mockPackagePolicy, + package: { + name: 'limited-package', + title: 'Limited package', + version: '0.0.0', + }, + inputs: [mockInput2], + }, ]) ).toEqual([ { @@ -238,12 +280,34 @@ describe('Fleet - storedPackagePoliciesToAgentInputs', () => { }, ], }, + { + id: 'some-uuid', + name: 'mock-package-policy', + revision: 1, + type: 'test-metrics', + data_stream: { namespace: 'default' }, + use_output: 'default', + meta: { + package: { + name: 'limited-package', + version: '0.0.0', + }, + }, + streams: [ + { + id: 'test-metrics-foo', + data_stream: { dataset: 'foo', type: 'metrics' }, + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], + }, + ], + }, ]); }); - it('returns agent inputs without streams', () => { + it('returns agent inputs without streams', async () => { expect( - storedPackagePoliciesToAgentInputs([ + await storedPackagePoliciesToAgentInputs(savedObjectsClientMock.create(), [ { ...mockPackagePolicy, package: { @@ -281,9 +345,9 @@ describe('Fleet - storedPackagePoliciesToAgentInputs', () => { ]); }); - it('returns agent inputs without disabled streams', () => { + it('returns agent inputs without disabled streams', async () => { expect( - storedPackagePoliciesToAgentInputs([ + await storedPackagePoliciesToAgentInputs(savedObjectsClientMock.create(), [ { ...mockPackagePolicy, inputs: [ @@ -314,9 +378,9 @@ describe('Fleet - storedPackagePoliciesToAgentInputs', () => { ]); }); - it('returns agent inputs with deeply merged config values', () => { + it('returns agent inputs with deeply merged config values', async () => { expect( - storedPackagePoliciesToAgentInputs([ + await storedPackagePoliciesToAgentInputs(savedObjectsClientMock.create(), [ { ...mockPackagePolicy, inputs: [ diff --git a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_inputs.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_inputs.ts new file mode 100644 index 0000000000000..f2c11c2067ee7 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_inputs.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { SavedObjectsClientContract } from 'kibana/server'; +import { merge } from 'lodash'; + +import { isPackageLimited } from '../../../common'; +import type { + PackagePolicy, + FullAgentPolicyInput, + FullAgentPolicyInputStream, + PackageInfo, +} from '../../types'; +import { DEFAULT_OUTPUT } from '../../constants'; + +import { getPackageInfo } from '../epm/packages'; + +const isPolicyEnabled = (packagePolicy: PackagePolicy) => { + return packagePolicy.enabled && packagePolicy.inputs && packagePolicy.inputs.length; +}; + +export const storedPackagePolicyToAgentInputs = ( + packagePolicy: PackagePolicy, + packageInfo?: PackageInfo, + outputId: string = DEFAULT_OUTPUT.name +): FullAgentPolicyInput[] => { + const fullInputs: FullAgentPolicyInput[] = []; + + if (!isPolicyEnabled(packagePolicy)) { + return fullInputs; + } + + // Marks to skip appending input information to package policy ID to make it unique if package is "limited": + // this means that only one policy for the package can exist on the agent policy, so its ID is already unique + const appendInputId = packageInfo && isPackageLimited(packageInfo) ? false : true; + + packagePolicy.inputs.forEach((input) => { + if (!input.enabled) { + return; + } + + const inputId = appendInputId + ? `${input.type}${input.policy_template ? `-${input.policy_template}-` : '-'}${ + packagePolicy.id + }` + : packagePolicy.id; + + const fullInput: FullAgentPolicyInput = { + id: inputId, + revision: packagePolicy.revision, + name: packagePolicy.name, + type: input.type, + data_stream: { + namespace: packagePolicy.namespace || 'default', + }, + use_output: outputId, + ...(input.compiled_input || {}), + ...(input.streams.length + ? { + streams: input.streams + .filter((stream) => stream.enabled) + .map((stream) => { + const fullStream: FullAgentPolicyInputStream = { + id: stream.id, + data_stream: stream.data_stream, + ...stream.compiled_stream, + ...Object.entries(stream.config || {}).reduce((acc, [key, { value }]) => { + acc[key] = value; + return acc; + }, {} as { [k: string]: any }), + }; + return fullStream; + }), + } + : {}), + }; + + // deeply merge the input.config values with the full policy input + merge( + fullInput, + Object.entries(input.config || {}).reduce( + (acc, [key, { value }]) => ({ ...acc, [key]: value }), + {} + ) + ); + + if (packagePolicy.package) { + fullInput.meta = { + package: { + name: packagePolicy.package.name, + version: packagePolicy.package.version, + }, + }; + } + + fullInputs.push(fullInput); + }); + + return fullInputs; +}; + +export const storedPackagePoliciesToAgentInputs = async ( + soClient: SavedObjectsClientContract, + packagePolicies: PackagePolicy[], + outputId: string = DEFAULT_OUTPUT.name +): Promise => { + const fullInputs: FullAgentPolicyInput[] = []; + + for (const packagePolicy of packagePolicies) { + if (!isPolicyEnabled(packagePolicy)) { + continue; + } + + const packageInfo = packagePolicy.package + ? await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: packagePolicy.package.name, + pkgVersion: packagePolicy.package.version, + }) + : undefined; + + fullInputs.push(...storedPackagePolicyToAgentInputs(packagePolicy, packageInfo, outputId)); + } + + return fullInputs; +}; diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index bd68b6c1d4b2a..81ecfda9425ab 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -61,6 +61,7 @@ import type { } from '../types'; import type { ExternalCallback } from '..'; +import { storedPackagePolicyToAgentInputs } from './agent_policies'; import { agentPolicyService } from './agent_policy'; import { outputService } from './output'; import { getPackageInfo, getInstallation, ensureInstalledPackage } from './epm/packages'; @@ -677,7 +678,7 @@ class PackagePolicyService implements PackagePolicyServiceInterface { pkgVersion )); - return this.calculateDiff(packagePolicy, packageInfo); + return this.calculateDiff(soClient, packagePolicy, packageInfo); } catch (error) { return { hasErrors: true, @@ -687,6 +688,7 @@ class PackagePolicyService implements PackagePolicyServiceInterface { } private async calculateDiff( + soClient: SavedObjectsClientContract, packagePolicy: PackagePolicy, packageInfo: PackageInfo ): Promise { @@ -722,6 +724,9 @@ class PackagePolicyService implements PackagePolicyServiceInterface { return { name: updatedPackagePolicy.name, diff: [packagePolicy, updatedPackagePolicy], + // TODO: Currently only returns the agent inputs for current package policy, not the upgraded one + // as we only show this version in the UI + agent_diff: [storedPackagePolicyToAgentInputs(packagePolicy, packageInfo)], hasErrors, }; } diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 9024cd05e2dea..6356ff8aa6cac 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -76,6 +76,7 @@ export type { FleetServerAgent, FleetServerAgentAction, FleetServerPolicy, + FullAgentPolicyInputStream, } from '../../common'; export { ElasticsearchAssetType, diff --git a/x-pack/plugins/infra/public/alerting/common/criterion_preview_chart/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/alerting/common/criterion_preview_chart/criterion_preview_chart.tsx index 3694e49bb7d02..67b6545691f78 100644 --- a/x-pack/plugins/infra/public/alerting/common/criterion_preview_chart/criterion_preview_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/common/criterion_preview_chart/criterion_preview_chart.tsx @@ -8,11 +8,11 @@ import React, { useMemo } from 'react'; import { niceTimeFormatter, TooltipValue } from '@elastic/charts'; import { Theme, LIGHT_THEME, DARK_THEME } from '@elastic/charts'; -import { sum, min as getMin, max as getMax } from 'lodash'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { sum, min as getMin, max as getMax } from 'lodash'; import { formatNumber } from '../../../../common/formatters/number'; import { GetLogAlertsChartPreviewDataSuccessResponsePayload } from '../../../../common/http_api'; @@ -72,6 +72,7 @@ export const getDomain = (series: Series, stacked: boolean = false) => { return { yMin: min || 0, yMax: max || 0, xMin: minTimestamp, xMax: maxTimestamp }; }; +// TODO use the EUI charts theme see src/plugins/charts/public/services/theme/README.md export const getChartTheme = (isDarkMode: boolean): Theme => { return isDarkMode ? DARK_THEME : LIGHT_THEME; }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts index fc6af920e008f..48fd23c0fce7f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts @@ -13,6 +13,7 @@ import { mergeWithDefaultTheme, } from '@elastic/charts'; +// TODO use the EUI charts theme see src/plugins/charts/public/services/theme/README.md export function getChartTheme(isDarkMode: boolean): Theme { return isDarkMode ? DARK_THEME : LIGHT_THEME; } diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index 127d798cb76c2..23bdf33f035a3 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -80,14 +80,7 @@ export async function initFieldsRoute(setup: CoreSetup) { }, }; - const runtimeMappings = indexPattern.fields - .filter((f) => f.runtimeField) - .reduce((acc, f) => { - if (!f.runtimeField) return acc; - // @ts-expect-error The MappingRuntimeField from @elastic/elasticsearch does not expose the "composite" runtime type yet - acc[f.name] = f.runtimeField; - return acc; - }, {} as Record); + const runtimeMappings = indexPattern.getRuntimeMappings(); const search = async (aggs: Record) => { const result = await requestClient.search({ diff --git a/x-pack/plugins/ml/common/types/trained_models.ts b/x-pack/plugins/ml/common/types/trained_models.ts index d6eda37f99465..3de759d9dbc87 100644 --- a/x-pack/plugins/ml/common/types/trained_models.ts +++ b/x-pack/plugins/ml/common/types/trained_models.ts @@ -205,6 +205,8 @@ export interface NodeDeploymentStatsResponse { total: number; jvm: number; }; + /** Max amount of memory available for ML */ + ml_max_in_bytes: number; /** Open anomaly detection jobs + hardcoded overhead */ anomaly_detection: { /** Total size in bytes */ @@ -226,6 +228,6 @@ export interface NodeDeploymentStatsResponse { } export interface NodesOverviewResponse { - count: number; + _nodes: { total: number; failed: number; successful: number }; nodes: NodeDeploymentStatsResponse[]; } diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss b/x-pack/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss index 4e04edd2e8e73..dfe7953dbf200 100644 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss @@ -9,6 +9,7 @@ &__list { margin: $euiSizeXS; + padding-bottom: $euiSizeXS; } &__header { diff --git a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx index dfd20359ca625..06bb5a363d36a 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx @@ -55,7 +55,10 @@ export const ColumnChart: FC = ({ {!isUnsupportedChartData(chartData) && data.length > 0 && (
- + - + {regressionBaselineData && ( - + = const dimensions = canvasRef.current.getBoundingClientRect(); - const startingXPos = Y_AXIS_LABEL_WIDTH + 2 * Y_AXIS_LABEL_PADDING; - const endingXPos = dimensions.width - X_AXIS_RIGHT_OVERFLOW; + const startingXPos = Y_AXIS_LABEL_WIDTH; + const endingXPos = dimensions.width; const svg = chartElement .append('svg') @@ -67,8 +66,9 @@ export const SwimlaneAnnotationContainer: FC = defaultMessage: 'Annotations', }) ) - .attr('x', Y_AXIS_LABEL_WIDTH + Y_AXIS_LABEL_PADDING) - .attr('y', ANNOTATION_CONTAINER_HEIGHT) + .attr('x', Y_AXIS_LABEL_WIDTH - Y_AXIS_LABEL_PADDING) + .attr('y', ANNOTATION_CONTAINER_HEIGHT / 2) + .attr('alignment-baseline', 'middle') .style('fill', euiTheme.euiTextSubduedColor) .style('font-size', euiTheme.euiFontSizeXS); diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 618a70435d0e2..7f0d7038f3f04 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -45,11 +45,7 @@ import { formatHumanReadableDateTime } from '../../../common/util/date_utils'; import './_explorer.scss'; import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control'; import { useUiSettings } from '../contexts/kibana'; -import { - Y_AXIS_LABEL_WIDTH, - Y_AXIS_LABEL_PADDING, - X_AXIS_RIGHT_OVERFLOW, -} from './swimlane_annotation_container'; +import { Y_AXIS_LABEL_WIDTH, Y_AXIS_LABEL_PADDING } from './swimlane_annotation_container'; import { useCurrentEuiTheme } from '../components/color_range_legend'; declare global { @@ -68,10 +64,10 @@ function getFormattedSeverityScore(score: number): string { * Ignore insignificant resize, e.g. browser scrollbar appearance. */ const RESIZE_THROTTLE_TIME_MS = 500; +const BORDER_WIDTH = 1; const CELL_HEIGHT = 30; const LEGEND_HEIGHT = 34; - -const Y_AXIS_HEIGHT = 24; +const X_AXIS_HEIGHT = 24; export const SWIM_LANE_LABEL_WIDTH = Y_AXIS_LABEL_WIDTH + 2 * Y_AXIS_LABEL_PADDING; @@ -248,9 +244,9 @@ export const SwimlaneContainer: FC = ({ return isLoading ? containerHeightRef.current : // TODO update when elastic charts X label will be fixed - rowsCount * CELL_HEIGHT + + rowsCount * (CELL_HEIGHT + BORDER_WIDTH * 2) + (showLegend ? LEGEND_HEIGHT : 0) + - (showYAxis ? Y_AXIS_HEIGHT : 0); + (showTimeline ? X_AXIS_HEIGHT : 0); }, [isLoading, rowsCount]); useEffect(() => { @@ -288,7 +284,7 @@ export const SwimlaneContainer: FC = ({ max: CELL_HEIGHT, }, stroke: { - width: 1, + width: BORDER_WIDTH, color: euiTheme.euiBorderColor, }, }, @@ -314,8 +310,6 @@ export const SwimlaneContainer: FC = ({ visible: showTimeline, textColor: euiTheme.euiTextSubduedColor, fontSize: parseInt(euiTheme.euiFontSizeXS, 10), - // Required to calculate where the swimlane ends - width: X_AXIS_RIGHT_OVERFLOW * 2, }, brushMask: { visible: showBrush, @@ -421,6 +415,7 @@ export const SwimlaneContainer: FC = ({ {showSwimlane && !isLoading && ( = ({ jobId, end, = ({
0} loading={loading}> - + diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx index f4165298bbdba..2da89a40b61b9 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx @@ -66,8 +66,12 @@ export const EventRateChart: FC = ({ 0} loading={loading}> {showAxis === true && } - - + {overlayRanges && overlayRanges.map((range, i) => ( diff --git a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/memory_preview_chart.tsx b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/memory_preview_chart.tsx index dd9b6f8253860..e355bc26f7fa7 100644 --- a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/memory_preview_chart.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/memory_preview_chart.tsx @@ -15,8 +15,10 @@ import { ScaleType, SeriesColorAccessor, Settings, + LineAnnotation, + AnnotationDomainType, } from '@elastic/charts'; -import { euiPaletteGray } from '@elastic/eui'; +import { EuiIcon, euiPaletteGray } from '@elastic/eui'; import { NodeDeploymentStatsResponse } from '../../../../common/types/trained_models'; import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; import { useCurrentEuiTheme } from '../../components/color_range_legend'; @@ -109,6 +111,7 @@ export const MemoryPreviewChart: FC = ({ memoryOverview return ( @@ -125,6 +128,22 @@ export const MemoryPreviewChart: FC = ({ memoryOverview tickFormat={(d: number) => bytesFormatter(d)} /> + } + markerPosition={Position.Top} + /> + = ({ compactView = false }) => { name: i18n.translate('xpack.ml.trainedModels.nodesList.nodeNameHeader', { defaultMessage: 'Name', }), + width: '200px', sortable: true, truncateText: true, 'data-test-subj': 'mlNodesTableColumnName', diff --git a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts index 11d5de7c45ed8..4ff555445f2c8 100644 --- a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts +++ b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts @@ -636,6 +636,9 @@ export function getMlClient( async validateDetector(...p: Parameters) { return mlClient.validateDetector(...p); }, + async getMemoryStats(...p: Parameters) { + return mlClient.getMemoryStats(...p); + }, ...searchProvider(client, jobSavedObjectService), } as MlClient; diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.test.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.test.ts index 22e803540f0d2..99919794001c9 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.test.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.test.ts @@ -5,129 +5,129 @@ * 2.0. */ -import { ModelService, modelsProvider } from './models_provider'; +import { MemoryStatsResponse, ModelService, modelsProvider } from './models_provider'; import { IScopedClusterClient } from 'kibana/server'; import { MlClient } from '../../lib/ml_client'; import mockResponse from './__mocks__/mock_deployment_response.json'; -import { MemoryOverviewService } from '../memory_overview/memory_overview_service'; describe('Model service', () => { const client = { - asInternalUser: { - nodes: { - stats: jest.fn(() => { - return Promise.resolve({ - _nodes: { - total: 3, - successful: 3, - failed: 0, - }, - cluster_name: 'test_cluster', - nodes: { - '3qIoLFnbSi-DwVrYioUCdw': { - timestamp: 1635167166946, - name: 'node3', - transport_address: '10.10.10.2:9353', - host: '10.10.10.2', - ip: '10.10.10.2:9353', - roles: ['data', 'ingest', 'master', 'ml', 'transform'], - attributes: { - 'ml.machine_memory': '15599742976', - 'xpack.installed': 'true', - 'ml.max_jvm_size': '1073741824', - }, - os: { - mem: { - total_in_bytes: 15599742976, - adjusted_total_in_bytes: 15599742976, - free_in_bytes: 376324096, - used_in_bytes: 15223418880, - free_percent: 2, - used_percent: 98, - }, - }, - }, - 'DpCy7SOBQla3pu0Dq-tnYw': { - timestamp: 1635167166946, - name: 'node2', - transport_address: '10.10.10.2:9352', - host: '10.10.10.2', - ip: '10.10.10.2:9352', - roles: ['data', 'master', 'ml', 'transform'], - attributes: { - 'ml.machine_memory': '15599742976', - 'xpack.installed': 'true', - 'ml.max_jvm_size': '1073741824', - }, - os: { - timestamp: 1635167166959, - mem: { - total_in_bytes: 15599742976, - adjusted_total_in_bytes: 15599742976, - free_in_bytes: 376324096, - used_in_bytes: 15223418880, - free_percent: 2, - used_percent: 98, - }, - }, - }, - 'pt7s6lKHQJaP4QHKtU-Q0Q': { - timestamp: 1635167166945, - name: 'node1', - transport_address: '10.10.10.2:9351', - host: '10.10.10.2', - ip: '10.10.10.2:9351', - roles: ['data', 'master', 'ml'], - attributes: { - 'ml.machine_memory': '15599742976', - 'xpack.installed': 'true', - 'ml.max_jvm_size': '1073741824', - }, - os: { - timestamp: 1635167166959, - mem: { - total_in_bytes: 15599742976, - adjusted_total_in_bytes: 15599742976, - free_in_bytes: 376324096, - used_in_bytes: 15223418880, - free_percent: 2, - used_percent: 98, - }, - }, - }, - }, - }); - }), - }, - }, + asInternalUser: {}, } as unknown as jest.Mocked; + const mlClient = { getTrainedModelsStats: jest.fn(() => { return Promise.resolve({ trained_model_stats: mockResponse, }); }), - } as unknown as jest.Mocked; - const memoryOverviewService = { - getDFAMemoryOverview: jest.fn(() => { - return Promise.resolve([{ job_id: '', node_id: '', model_size: 32165465 }]); - }), - getAnomalyDetectionMemoryOverview: jest.fn(() => { - return Promise.resolve([{ job_id: '', node_id: '', model_size: 32165465 }]); + getMemoryStats: jest.fn(() => { + return Promise.resolve({ + _nodes: { + total: 3, + successful: 3, + failed: 0, + }, + cluster_name: 'test_cluster', + nodes: { + '3qIoLFnbSi-DwVrYioUCdw': { + name: 'node3', + transport_address: '10.10.10.2:9353', + roles: ['data', 'ingest', 'master', 'ml', 'transform'], + attributes: { + 'ml.machine_memory': '15599742976', + 'ml.max_jvm_size': '1073741824', + }, + jvm: { + heap_max_in_bytes: 1073741824, + java_inference_in_bytes: 0, + java_inference_max_in_bytes: 0, + }, + mem: { + adjusted_total_in_bytes: 15599742976, + total_in_bytes: 15599742976, + ml: { + data_frame_analytics_in_bytes: 0, + native_code_overhead_in_bytes: 0, + max_in_bytes: 1073741824, + anomaly_detectors_in_bytes: 0, + native_inference_in_bytes: 1555161790, + }, + }, + ephemeral_id: '3qIoLFnbSi-DwVrYioUCdw', + }, + 'DpCy7SOBQla3pu0Dq-tnYw': { + name: 'node2', + transport_address: '10.10.10.2:9352', + roles: ['data', 'master', 'ml', 'transform'], + attributes: { + 'ml.machine_memory': '15599742976', + 'ml.max_jvm_size': '1073741824', + }, + jvm: { + heap_max_in_bytes: 1073741824, + java_inference_in_bytes: 0, + java_inference_max_in_bytes: 0, + }, + mem: { + adjusted_total_in_bytes: 15599742976, + total_in_bytes: 15599742976, + ml: { + data_frame_analytics_in_bytes: 0, + native_code_overhead_in_bytes: 0, + max_in_bytes: 1073741824, + anomaly_detectors_in_bytes: 0, + native_inference_in_bytes: 1555161790, + }, + }, + ephemeral_id: '3qIoLFnbSi-DwVrYioUCdw', + }, + 'pt7s6lKHQJaP4QHKtU-Q0Q': { + name: 'node1', + transport_address: '10.10.10.2:9351', + roles: ['data', 'master', 'ml'], + attributes: { + 'ml.machine_memory': '15599742976', + 'ml.max_jvm_size': '1073741824', + }, + jvm: { + heap_max_in_bytes: 1073741824, + java_inference_in_bytes: 0, + java_inference_max_in_bytes: 0, + }, + mem: { + adjusted_total_in_bytes: 15599742976, + total_in_bytes: 15599742976, + ml: { + data_frame_analytics_in_bytes: 0, + native_code_overhead_in_bytes: 0, + max_in_bytes: 1073741824, + anomaly_detectors_in_bytes: 0, + native_inference_in_bytes: 1555161790, + }, + }, + ephemeral_id: '3qIoLFnbSi-DwVrYioUCdw', + }, + }, + } as MemoryStatsResponse); }), - } as unknown as jest.Mocked; + } as unknown as jest.Mocked; let service: ModelService; beforeEach(() => { - service = modelsProvider(client, mlClient, memoryOverviewService); + service = modelsProvider(client, mlClient); }); afterEach(() => {}); it('extract nodes list correctly', async () => { expect(await service.getNodesOverview()).toEqual({ - count: 3, + _nodes: { + failed: 0, + successful: 3, + total: 3, + }, nodes: [ { name: 'node3', @@ -219,6 +219,7 @@ describe('Model service', () => { }, id: '3qIoLFnbSi-DwVrYioUCdw', memory_overview: { + ml_max_in_bytes: 1073741824, anomaly_detection: { total: 0, }, @@ -339,6 +340,7 @@ describe('Model service', () => { }, id: 'DpCy7SOBQla3pu0Dq-tnYw', memory_overview: { + ml_max_in_bytes: 1073741824, anomaly_detection: { total: 0, }, @@ -462,6 +464,7 @@ describe('Model service', () => { }, id: 'pt7s6lKHQJaP4QHKtU-Q0Q', memory_overview: { + ml_max_in_bytes: 1073741824, anomaly_detection: { total: 0, }, diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.ts index 714d02704ff92..db0eee9ec757c 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.ts @@ -6,7 +6,7 @@ */ import type { IScopedClusterClient } from 'kibana/server'; -import { sumBy, pick } from 'lodash'; +import { pick } from 'lodash'; import { MlTrainedModelStats, NodesInfoNodeInfo, @@ -17,20 +17,15 @@ import type { NodesOverviewResponse, } from '../../../common/types/trained_models'; import type { MlClient } from '../../lib/ml_client'; -import { - MemoryOverviewService, - NATIVE_EXECUTABLE_CODE_OVERHEAD, -} from '../memory_overview/memory_overview_service'; import { TrainedModelDeploymentStatsResponse, TrainedModelModelSizeStats, } from '../../../common/types/trained_models'; import { isDefined } from '../../../common/types/guards'; -import { isPopulatedObject } from '../../../common'; export type ModelService = ReturnType; -const NODE_FIELDS = ['attributes', 'name', 'roles', 'version'] as const; +const NODE_FIELDS = ['attributes', 'name', 'roles'] as const; export type RequiredNodeFields = Pick; @@ -40,11 +35,38 @@ interface TrainedModelStatsResponse extends MlTrainedModelStats { model_size_stats?: TrainedModelModelSizeStats; } -export function modelsProvider( - client: IScopedClusterClient, - mlClient: MlClient, - memoryOverviewService?: MemoryOverviewService -) { +export interface MemoryStatsResponse { + _nodes: { total: number; failed: number; successful: number }; + cluster_name: string; + nodes: Record< + string, + { + jvm: { + heap_max_in_bytes: number; + java_inference_in_bytes: number; + java_inference_max_in_bytes: number; + }; + mem: { + adjusted_total_in_bytes: number; + total_in_bytes: number; + ml: { + data_frame_analytics_in_bytes: number; + native_code_overhead_in_bytes: number; + max_in_bytes: number; + anomaly_detectors_in_bytes: number; + native_inference_in_bytes: number; + }; + }; + transport_address: string; + roles: string[]; + name: string; + attributes: Record<`${'ml.'}${string}`, string>; + ephemeral_id: string; + } + >; +} + +export function modelsProvider(client: IScopedClusterClient, mlClient: MlClient) { return { /** * Retrieves the map of model ids and aliases with associated pipelines. @@ -89,30 +111,19 @@ export function modelsProvider( * Provides the ML nodes overview with allocated models. */ async getNodesOverview(): Promise { - if (!memoryOverviewService) { - throw new Error('Memory overview service is not provided'); - } + const response = (await mlClient.getMemoryStats()) as MemoryStatsResponse; const { trained_model_stats: trainedModelStats } = await mlClient.getTrainedModelsStats({ size: 10000, }); - const { nodes: clusterNodes } = await client.asInternalUser.nodes.stats(); - - const mlNodes = Object.entries(clusterNodes).filter(([, node]) => node.roles?.includes('ml')); - - const adMemoryReport = await memoryOverviewService.getAnomalyDetectionMemoryOverview(); - const dfaMemoryReport = await memoryOverviewService.getDFAMemoryOverview(); + const mlNodes = Object.entries(response.nodes); const nodeDeploymentStatsResponses: NodeDeploymentStatsResponse[] = mlNodes.map( ([nodeId, node]) => { const nodeFields = pick(node, NODE_FIELDS) as RequiredNodeFields; - nodeFields.attributes = isPopulatedObject(nodeFields.attributes) - ? Object.fromEntries( - Object.entries(nodeFields.attributes).filter(([id]) => id.startsWith('ml')) - ) - : nodeFields.attributes; + nodeFields.attributes = nodeFields.attributes; const allocatedModels = (trainedModelStats as TrainedModelStatsResponse[]) .filter( @@ -150,15 +161,9 @@ export function modelsProvider( }); const memoryRes = { - adTotalMemory: sumBy( - adMemoryReport.filter((ad) => ad.node_id === nodeId), - 'model_size' - ), - dfaTotalMemory: sumBy( - dfaMemoryReport.filter((dfa) => dfa.node_id === nodeId), - 'model_size' - ), - trainedModelsTotalMemory: sumBy(modelsMemoryUsage, 'model_size'), + adTotalMemory: node.mem.ml.anomaly_detectors_in_bytes, + dfaTotalMemory: node.mem.ml.data_frame_analytics_in_bytes, + trainedModelsTotalMemory: node.mem.ml.native_inference_in_bytes, }; for (const key of Object.keys(memoryRes)) { @@ -168,7 +173,7 @@ export function modelsProvider( * ML job to run on a given node will do this, and then subsequent ML jobs on the same node will reuse the * same already-loaded code. */ - memoryRes[key as keyof typeof memoryRes] += NATIVE_EXECUTABLE_CODE_OVERHEAD; + memoryRes[key as keyof typeof memoryRes] += node.mem.ml.native_code_overhead_in_bytes; break; } } @@ -179,10 +184,8 @@ export function modelsProvider( allocated_models: allocatedModels, memory_overview: { machine_memory: { - // TODO remove ts-ignore when elasticsearch client is updated - // @ts-ignore - total: Number(node.os?.mem.adjusted_total_in_bytes ?? node.os?.mem.total_in_bytes), - jvm: Number(node.attributes!['ml.max_jvm_size']), + total: node.mem.adjusted_total_in_bytes, + jvm: node.jvm.heap_max_in_bytes, }, anomaly_detection: { total: memoryRes.adTotalMemory, @@ -194,13 +197,14 @@ export function modelsProvider( total: memoryRes.trainedModelsTotalMemory, by_model: modelsMemoryUsage, }, + ml_max_in_bytes: node.mem.ml.max_in_bytes, }, }; } ); return { - count: nodeDeploymentStatsResponses.length, + _nodes: response._nodes, nodes: nodeDeploymentStatsResponses, }; }, diff --git a/x-pack/plugins/ml/server/models/memory_overview/memory_overview_service.ts b/x-pack/plugins/ml/server/models/memory_overview/memory_overview_service.ts deleted file mode 100644 index d7f6eb584f7fe..0000000000000 --- a/x-pack/plugins/ml/server/models/memory_overview/memory_overview_service.ts +++ /dev/null @@ -1,85 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import numeral from '@elastic/numeral'; -import { keyBy } from 'lodash'; -import { MlClient } from '../../lib/ml_client'; - -export type MemoryOverviewService = ReturnType; - -export interface MlJobMemoryOverview { - job_id: string; - node_id: string; - model_size: number; -} - -const MB = Math.pow(2, 20); - -const AD_PROCESS_MEMORY_OVERHEAD = 10 * MB; -const DFA_PROCESS_MEMORY_OVERHEAD = 5 * MB; -export const NATIVE_EXECUTABLE_CODE_OVERHEAD = 30 * MB; - -/** - * Provides a service for memory overview across ML. - * @param mlClient - */ -export function memoryOverviewServiceProvider(mlClient: MlClient) { - return { - /** - * Retrieves memory consumed my started DFA jobs. - */ - async getDFAMemoryOverview(): Promise { - const { data_frame_analytics: dfaStats } = await mlClient.getDataFrameAnalyticsStats(); - - const dfaMemoryReport = dfaStats - .filter((dfa) => dfa.state === 'started') - .map((dfa) => { - return { - node_id: dfa.node?.id, - job_id: dfa.id, - }; - }) as MlJobMemoryOverview[]; - - if (dfaMemoryReport.length === 0) { - return []; - } - - const dfaMemoryKeyByJobId = keyBy(dfaMemoryReport, 'job_id'); - - const { data_frame_analytics: startedDfaJobs } = await mlClient.getDataFrameAnalytics({ - id: dfaMemoryReport.map((v) => v.job_id).join(','), - }); - - startedDfaJobs.forEach((dfa) => { - dfaMemoryKeyByJobId[dfa.id].model_size = - numeral( - dfa.model_memory_limit?.toUpperCase() - // @ts-ignore - ).value() + DFA_PROCESS_MEMORY_OVERHEAD; - }); - - return dfaMemoryReport; - }, - /** - * Retrieves memory consumed by opened Anomaly Detection jobs. - */ - async getAnomalyDetectionMemoryOverview(): Promise { - const { jobs: jobsStats } = await mlClient.getJobStats(); - - return jobsStats - .filter((v) => v.state === 'opened') - .map((jobStats) => { - return { - node_id: jobStats.node!.id, - // @ts-expect-error model_bytes can be string | number, cannot sum it with AD_PROCESS_MEMORY_OVERHEAD - model_size: jobStats.model_size_stats.model_bytes + AD_PROCESS_MEMORY_OVERHEAD, - job_id: jobStats.job_id, - }; - }); - }, - }; -} diff --git a/x-pack/plugins/ml/server/routes/trained_models.ts b/x-pack/plugins/ml/server/routes/trained_models.ts index 2cbf9a4dde763..887ad47f1ceb2 100644 --- a/x-pack/plugins/ml/server/routes/trained_models.ts +++ b/x-pack/plugins/ml/server/routes/trained_models.ts @@ -16,7 +16,6 @@ import { } from './schemas/inference_schema'; import { modelsProvider } from '../models/data_frame_analytics'; import { TrainedModelConfigResponse } from '../../common/types/trained_models'; -import { memoryOverviewServiceProvider } from '../models/memory_overview'; import { mlLog } from '../lib/log'; import { forceQuerySchema } from './schemas/anomaly_detectors_schema'; @@ -278,12 +277,7 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) }, routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => { try { - const memoryOverviewService = memoryOverviewServiceProvider(mlClient); - const result = await modelsProvider( - client, - mlClient, - memoryOverviewService - ).getNodesOverview(); + const result = await modelsProvider(client, mlClient).getNodesOverview(); return response.ok({ body: result, }); diff --git a/x-pack/plugins/monitoring/server/config.test.ts b/x-pack/plugins/monitoring/server/config.test.ts index 1e1d8c8f25602..699ea739d6a73 100644 --- a/x-pack/plugins/monitoring/server/config.test.ts +++ b/x-pack/plugins/monitoring/server/config.test.ts @@ -78,6 +78,7 @@ describe('config schema', () => { "ignoreVersionMismatch": false, "logFetchCount": 10, "logQueries": false, + "maxSockets": Infinity, "pingTimeout": "PT30S", "requestHeadersWhitelist": Array [ "authorization", diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts b/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts new file mode 100644 index 0000000000000..86b445dd9a1e6 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_fetch_rules.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState, useCallback } from 'react'; +import { loadRules, Rule } from '../../../triggers_actions_ui/public'; +import { RULES_LOAD_ERROR } from '../pages/rules/translations'; +import { FetchRulesProps } from '../pages/rules/types'; +import { OBSERVABILITY_RULE_TYPES } from '../pages/rules/config'; +import { useKibana } from '../utils/kibana_react'; + +interface RuleState { + isLoading: boolean; + data: Rule[]; + error: string | null; + totalItemCount: number; +} + +export function useFetchRules({ ruleLastResponseFilter, page, sort }: FetchRulesProps) { + const { http } = useKibana().services; + + const [rulesState, setRulesState] = useState({ + isLoading: false, + data: [], + error: null, + totalItemCount: 0, + }); + + const fetchRules = useCallback(async () => { + setRulesState((oldState) => ({ ...oldState, isLoading: true })); + + try { + const response = await loadRules({ + http, + page, + typesFilter: OBSERVABILITY_RULE_TYPES, + ruleStatusesFilter: ruleLastResponseFilter, + sort, + }); + setRulesState((oldState) => ({ + ...oldState, + isLoading: false, + data: response.data, + totalItemCount: response.total, + })); + } catch (_e) { + setRulesState((oldState) => ({ ...oldState, isLoading: false, error: RULES_LOAD_ERROR })); + } + }, [http, page, ruleLastResponseFilter, sort]); + useEffect(() => { + fetchRules(); + }, [fetchRules]); + + return { + rulesState, + reload: fetchRules, + setRulesState, + }; +} diff --git a/x-pack/plugins/observability/public/pages/rules/components/delete_modal_confirmation.tsx b/x-pack/plugins/observability/public/pages/rules/components/delete_modal_confirmation.tsx new file mode 100644 index 0000000000000..b09d31ba59c78 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/components/delete_modal_confirmation.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiConfirmModal } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { HttpSetup } from 'kibana/public'; +import { useKibana } from '../../../utils/kibana_react'; +import { + confirmModalText, + confirmButtonText, + cancelButtonText, + deleteSuccessText, + deleteErrorText, +} from '../translations'; + +export function DeleteModalConfirmation({ + idsToDelete, + apiDeleteCall, + onDeleted, + onCancel, + onErrors, + singleTitle, + multipleTitle, + setIsLoadingState, +}: { + idsToDelete: string[]; + apiDeleteCall: ({ + ids, + http, + }: { + ids: string[]; + http: HttpSetup; + }) => Promise<{ successes: string[]; errors: string[] }>; + onDeleted: (deleted: string[]) => void; + onCancel: () => void; + onErrors: () => void; + singleTitle: string; + multipleTitle: string; + setIsLoadingState: (isLoading: boolean) => void; +}) { + const [deleteModalFlyoutVisible, setDeleteModalVisibility] = useState(false); + + useEffect(() => { + setDeleteModalVisibility(idsToDelete.length > 0); + }, [idsToDelete]); + + const { + http, + notifications: { toasts }, + } = useKibana().services; + const numIdsToDelete = idsToDelete.length; + if (!deleteModalFlyoutVisible) { + return null; + } + + return ( + { + setDeleteModalVisibility(false); + onCancel(); + }} + onConfirm={async () => { + setDeleteModalVisibility(false); + setIsLoadingState(true); + const { successes, errors } = await apiDeleteCall({ ids: idsToDelete, http }); + setIsLoadingState(false); + + const numSuccesses = successes.length; + const numErrors = errors.length; + if (numSuccesses > 0) { + toasts.addSuccess(deleteSuccessText(numSuccesses, singleTitle, multipleTitle)); + } + + if (numErrors > 0) { + toasts.addDanger(deleteErrorText(numErrors, singleTitle, multipleTitle)); + await onErrors(); + } + await onDeleted(successes); + }} + cancelButtonText={cancelButtonText} + confirmButtonText={confirmButtonText(numIdsToDelete, singleTitle, multipleTitle)} + > + {confirmModalText(numIdsToDelete, singleTitle, multipleTitle)} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/rules/components/edit_rule_flyout.tsx b/x-pack/plugins/observability/public/pages/rules/components/edit_rule_flyout.tsx new file mode 100644 index 0000000000000..89dce3e1975fd --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/components/edit_rule_flyout.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useMemo, useEffect } from 'react'; +import { useKibana } from '../../../utils/kibana_react'; +import { EditFlyoutProps } from '../types'; + +export function EditRuleFlyout({ currentRule, onSave }: EditFlyoutProps) { + const { triggersActionsUi } = useKibana().services; + const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); + + useEffect(() => { + setEditFlyoutVisibility(true); + }, [currentRule]); + const EditAlertFlyout = useMemo( + () => + triggersActionsUi.getEditAlertFlyout({ + initialRule: currentRule, + onClose: () => { + setEditFlyoutVisibility(false); + }, + onSave, + }), + [currentRule, setEditFlyoutVisibility, triggersActionsUi, onSave] + ); + return <>{editFlyoutVisible && EditAlertFlyout}; +} diff --git a/x-pack/plugins/observability/public/pages/rules/components/execution_status.tsx b/x-pack/plugins/observability/public/pages/rules/components/execution_status.tsx new file mode 100644 index 0000000000000..4cdcabe574396 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/components/execution_status.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiHealth, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { AlertExecutionStatusErrorReasons } from '../../../../../alerting/common'; +import { getHealthColor, rulesStatusesTranslationsMapping } from '../config'; +import { RULE_STATUS_LICENSE_ERROR } from '../translations'; +import { ExecutionStatusProps } from '../types'; + +export function ExecutionStatus({ executionStatus }: ExecutionStatusProps) { + const healthColor = getHealthColor(executionStatus.status); + const tooltipMessage = + executionStatus.status === 'error' ? `Error: ${executionStatus?.error?.message}` : null; + const isLicenseError = executionStatus.error?.reason === AlertExecutionStatusErrorReasons.License; + const statusMessage = isLicenseError + ? RULE_STATUS_LICENSE_ERROR + : rulesStatusesTranslationsMapping[executionStatus.status]; + + const health = ( + + {statusMessage} + + ); + + const healthWithTooltip = tooltipMessage ? ( + + {health} + + ) : ( + health + ); + + return ( + + {healthWithTooltip} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/rules/components/last_response_filter.tsx b/x-pack/plugins/observability/public/pages/rules/components/last_response_filter.tsx new file mode 100644 index 0000000000000..5a9be48252909 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/components/last_response_filter.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable react/function-component-definition */ + +import React, { useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiFilterGroup, + EuiPopover, + EuiFilterButton, + EuiFilterSelectItem, + EuiHealth, +} from '@elastic/eui'; +import { AlertExecutionStatuses, AlertExecutionStatusValues } from '../../../../../alerting/common'; +import { getHealthColor, rulesStatusesTranslationsMapping } from '../config'; +import { StatusFilterProps } from '../types'; + +export const LastResponseFilter: React.FunctionComponent = ({ + selectedStatuses, + onChange, +}: StatusFilterProps) => { + const [selectedValues, setSelectedValues] = useState(selectedStatuses); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + useEffect(() => { + if (onChange) { + onChange(selectedValues); + } + }, [selectedValues, onChange]); + + useEffect(() => { + setSelectedValues(selectedStatuses); + }, [selectedStatuses]); + + return ( + + setIsPopoverOpen(false)} + button={ + 0} + numActiveFilters={selectedValues.length} + numFilters={selectedValues.length} + onClick={() => setIsPopoverOpen(!isPopoverOpen)} + data-test-subj="ruleStatusFilterButton" + > + + + } + > +
+ {[...AlertExecutionStatusValues].sort().map((item: AlertExecutionStatuses) => { + const healthColor = getHealthColor(item); + return ( + { + const isPreviouslyChecked = selectedValues.includes(item); + if (isPreviouslyChecked) { + setSelectedValues(selectedValues.filter((val) => val !== item)); + } else { + setSelectedValues(selectedValues.concat(item)); + } + }} + checked={selectedValues.includes(item) ? 'on' : undefined} + data-test-subj={`ruleStatus${item}FilerOption`} + > + {rulesStatusesTranslationsMapping[item]} + + ); + })} +
+
+
+ ); +}; diff --git a/x-pack/plugins/observability/public/pages/rules/components/last_run.tsx b/x-pack/plugins/observability/public/pages/rules/components/last_run.tsx new file mode 100644 index 0000000000000..08bb6fb229b94 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/components/last_run.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import moment from 'moment'; +import { LastRunProps } from '../types'; + +export function LastRun({ date }: LastRunProps) { + return ( + <> + + + + {moment(date).fromNow()} + + + + + ); +} diff --git a/x-pack/plugins/observability/public/pages/rules/components/name.tsx b/x-pack/plugins/observability/public/pages/rules/components/name.tsx new file mode 100644 index 0000000000000..2b1f831256910 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/components/name.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiBadge } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { RuleNameProps } from '../types'; +import { useKibana } from '../../../utils/kibana_react'; + +export function Name({ name, rule }: RuleNameProps) { + const { http } = useKibana().services; + const detailsLink = http.basePath.prepend( + `/app/management/insightsAndAlerting/triggersActions/rule/${rule.id}` + ); + const link = ( + + + + + + {name} + + + + + + + {rule.ruleType} + + + + ); + return ( + <> + {link} + {rule.enabled && rule.muteAll && ( + + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/rules/components/rules_table.tsx b/x-pack/plugins/observability/public/pages/rules/components/rules_table.tsx new file mode 100644 index 0000000000000..db1febf8d3edd --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/components/rules_table.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback } from 'react'; +import { EuiBasicTable, EuiSpacer, EuiTableSortingType } from '@elastic/eui'; +import { RulesTableProps } from '../types'; +import { RuleTableItem } from '../../../../../triggers_actions_ui/public'; + +export interface Pagination { + index: number; + size: number; +} + +export function RulesTable({ + columns, + rules, + page, + totalItemCount, + onPageChange, + sort, + onSortChange, + isLoading, +}: RulesTableProps) { + const onChange = useCallback( + ({ + page: changedPage, + sort: changedSort, + }: { + page?: Pagination; + sort?: EuiTableSortingType['sort']; + }) => { + if (changedPage) { + onPageChange(changedPage); + } + if (changedSort) { + onSortChange(changedSort); + } + }, + [onPageChange, onSortChange] + ); + return ( +
+ + <> + + + + +
+ ); +} diff --git a/x-pack/plugins/observability/public/pages/rules/components/status.tsx b/x-pack/plugins/observability/public/pages/rules/components/status.tsx new file mode 100644 index 0000000000000..abc2dc8bfa492 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/components/status.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiBadge } from '@elastic/eui'; +import { StatusProps } from '../types'; +import { statusMap } from '../config'; + +export function Status({ type, onClick }: StatusProps) { + return ( + + {statusMap[type].label} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/rules/components/status_context.tsx b/x-pack/plugins/observability/public/pages/rules/components/status_context.tsx new file mode 100644 index 0000000000000..49761d7c43154 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/components/status_context.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useCallback, useMemo } from 'react'; +import { + EuiPopover, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { Status } from './status'; +import { RuleStatus, StatusContextProps } from '../types'; +import { statusMap } from '../config'; + +export function StatusContext({ + item, + onStatusChanged, + enableRule, + disableRule, + muteRule, +}: StatusContextProps) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [isUpdating, setIsUpdating] = useState(false); + const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); + + const currentStatus = item.enabled ? RuleStatus.enabled : RuleStatus.disabled; + const popOverButton = useMemo( + () => , + [currentStatus, togglePopover] + ); + + const onContextMenuItemClick = useCallback( + async (status: RuleStatus) => { + togglePopover(); + if (currentStatus !== status) { + setIsUpdating(true); + + if (status === RuleStatus.enabled) { + await enableRule({ ...item, enabled: true }); + } else if (status === RuleStatus.disabled) { + await disableRule({ ...item, enabled: false }); + } + setIsUpdating(false); + onStatusChanged(status); + } + }, + [item, togglePopover, enableRule, disableRule, currentStatus, onStatusChanged] + ); + const panelItems = useMemo( + () => + Object.values(RuleStatus).map((status: RuleStatus) => ( + onContextMenuItemClick(status)} + > + {statusMap[status].label} + + )), + [currentStatus, onContextMenuItemClick] + ); + + return isUpdating ? ( + + ) : ( + setIsPopoverOpen(false)} + anchorPosition="downLeft" + isOpen={isPopoverOpen} + panelPaddingSize="none" + > + + + ); +} diff --git a/x-pack/plugins/observability/public/pages/rules/config.ts b/x-pack/plugins/observability/public/pages/rules/config.ts new file mode 100644 index 0000000000000..0296fdb73b951 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/config.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Status, RuleStatus } from './types'; +import { + RULE_STATUS_OK, + RULE_STATUS_ACTIVE, + RULE_STATUS_ERROR, + RULE_STATUS_PENDING, + RULE_STATUS_UNKNOWN, +} from './translations'; +import { AlertExecutionStatuses } from '../../../../alerting/common'; +import { Rule, RuleTypeIndex, RuleType } from '../../../../triggers_actions_ui/public'; + +export const statusMap: Status = { + [RuleStatus.enabled]: { + color: 'primary', + label: 'Enabled', + }, + [RuleStatus.disabled]: { + color: 'default', + label: 'Disabled', + }, +}; + +export const DEFAULT_SEARCH_PAGE_SIZE: number = 25; + +export function getHealthColor(status: AlertExecutionStatuses) { + switch (status) { + case 'active': + return 'success'; + case 'error': + return 'danger'; + case 'ok': + return 'primary'; + case 'pending': + return 'accent'; + default: + return 'subdued'; + } +} + +export const rulesStatusesTranslationsMapping = { + ok: RULE_STATUS_OK, + active: RULE_STATUS_ACTIVE, + error: RULE_STATUS_ERROR, + pending: RULE_STATUS_PENDING, + unknown: RULE_STATUS_UNKNOWN, +}; + +export const OBSERVABILITY_RULE_TYPES = [ + 'xpack.uptime.alerts.monitorStatus', + 'xpack.uptime.alerts.tls', + 'xpack.uptime.alerts.tlsCertificate', + 'xpack.uptime.alerts.durationAnomaly', + 'apm.error_rate', + 'apm.transaction_error_rate', + 'apm.transaction_duration', + 'apm.transaction_duration_anomaly', + 'metrics.alert.inventory.threshold', + 'metrics.alert.threshold', + 'logs.alert.document.count', +]; + +export const OBSERVABILITY_SOLUTIONS = ['logs', 'uptime', 'infrastructure', 'apm']; + +export type InitialRule = Partial & + Pick; + +export function hasAllPrivilege(rule: InitialRule, ruleType?: RuleType): boolean { + return ruleType?.authorizedConsumers[rule.consumer]?.all ?? false; +} + +export function convertRulesToTableItems( + rules: Rule[], + ruleTypeIndex: RuleTypeIndex, + canExecuteActions: boolean +) { + return rules.map((rule, index: number) => ({ + ...rule, + index, + actionsCount: rule.actions.length, + ruleType: ruleTypeIndex.get(rule.ruleTypeId)?.name ?? rule.ruleTypeId, + isEditable: + hasAllPrivilege(rule, ruleTypeIndex.get(rule.ruleTypeId)) && + (canExecuteActions || (!canExecuteActions && !rule.actions.length)), + enabledInLicense: !!ruleTypeIndex.get(rule.ruleTypeId)?.enabledInLicense, + })); +} diff --git a/x-pack/plugins/observability/public/pages/rules/index.tsx b/x-pack/plugins/observability/public/pages/rules/index.tsx index d4eba98b8c5cb..d6f932baeefba 100644 --- a/x-pack/plugins/observability/public/pages/rules/index.tsx +++ b/x-pack/plugins/observability/public/pages/rules/index.tsx @@ -5,199 +5,213 @@ * 2.0. */ -import React, { useState, useEffect, useCallback } from 'react'; -import moment from 'moment'; +import React, { useState, useMemo } from 'react'; import { - EuiBasicTable, + EuiButton, + EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiText, - EuiBadge, - EuiPopover, - EuiContextMenuPanel, - EuiContextMenuItem, EuiHorizontalRule, EuiAutoRefreshButton, + EuiTableSortingType, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; - import { useKibana } from '../../utils/kibana_react'; - -const DEFAULT_SEARCH_PAGE_SIZE: number = 25; - -interface RuleState { - data: []; - totalItemsCount: number; -} - -interface Pagination { - index: number; - size: number; -} +import { useFetchRules } from '../../hooks/use_fetch_rules'; +import { RulesTable } from './components/rules_table'; +import { Name } from './components/name'; +import { LastResponseFilter } from './components/last_response_filter'; +import { StatusContext } from './components/status_context'; +import { ExecutionStatus } from './components/execution_status'; +import { LastRun } from './components/last_run'; +import { EditRuleFlyout } from './components/edit_rule_flyout'; +import { DeleteModalConfirmation } from './components/delete_modal_confirmation'; +import { + deleteRules, + RuleTableItem, + enableRule, + disableRule, + muteRule, + useLoadRuleTypes, +} from '../../../../triggers_actions_ui/public'; +import { AlertExecutionStatus, ALERTS_FEATURE_ID } from '../../../../alerting/common'; +import { Pagination } from './types'; +import { + DEFAULT_SEARCH_PAGE_SIZE, + convertRulesToTableItems, + OBSERVABILITY_SOLUTIONS, +} from './config'; +import { + LAST_RESPONSE_COLUMN_TITLE, + LAST_RUN_COLUMN_TITLE, + RULE_COLUMN_TITLE, + STATUS_COLUMN_TITLE, + ACTIONS_COLUMN_TITLE, + EDIT_ACTION_ARIA_LABEL, + EDIT_ACTION_TOOLTIP, + DELETE_ACTION_TOOLTIP, + DELETE_ACTION_ARIA_LABEL, + RULES_PAGE_TITLE, + RULES_BREADCRUMB_TEXT, + RULES_SINGLE_TITLE, + RULES_PLURAL_TITLE, +} from './translations'; export function RulesPage() { const { ObservabilityPageTemplate } = usePluginContext(); const { http, docLinks, + triggersActionsUi, notifications: { toasts }, } = useKibana().services; - const [rules, setRules] = useState({ data: [], totalItemsCount: 0 }); const [page, setPage] = useState({ index: 0, size: DEFAULT_SEARCH_PAGE_SIZE }); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [sort, setSort] = useState['sort']>({ + field: 'name', + direction: 'asc', + }); + const [ruleLastResponseFilter, setRuleLastResponseFilter] = useState([]); + const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); + const [rulesToDelete, setRulesToDelete] = useState([]); + const [createRuleFlyoutVisibility, setCreateRuleFlyoutVisibility] = useState(false); - async function loadObservabilityRules() { - const { loadRules } = await import('../../../../triggers_actions_ui/public'); - try { - const response = await loadRules({ - http, - page: { index: 0, size: DEFAULT_SEARCH_PAGE_SIZE }, - typesFilter: [ - 'xpack.uptime.alerts.monitorStatus', - 'xpack.uptime.alerts.tls', - 'xpack.uptime.alerts.tlsCertificate', - 'xpack.uptime.alerts.durationAnomaly', - 'apm.error_rate', - 'apm.transaction_error_rate', - 'apm.transaction_duration', - 'apm.transaction_duration_anomaly', - 'metrics.alert.inventory.threshold', - 'metrics.alert.threshold', - 'logs.alert.document.count', - ], - }); - setRules({ - data: response.data as any, - totalItemsCount: response.total, - }); - } catch (_e) { - toasts.addDanger({ - title: i18n.translate('xpack.observability.rules.loadError', { - defaultMessage: 'Unable to load rules', - }), - }); - } - } + const onRuleEdit = (ruleItem: RuleTableItem) => { + setCurrentRuleToEdit(ruleItem); + }; - enum RuleStatus { - enabled = 'enabled', - disabled = 'disabled', - } - - const statuses = Object.values(RuleStatus); - const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); - const popOverButton = ( - - Enabled - - ); - - const panelItems = statuses.map((status) => ( - - {status} - - )); - - useEffect(() => { - loadObservabilityRules(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const { rulesState, setRulesState, reload } = useFetchRules({ + ruleLastResponseFilter, + page, + sort, + }); + const { data: rules, totalItemCount, error } = rulesState; + const { ruleTypeIndex } = useLoadRuleTypes({ filteredSolutions: OBSERVABILITY_SOLUTIONS }); useBreadcrumbs([ { - text: i18n.translate('xpack.observability.breadcrumbs.rulesLinkText', { - defaultMessage: 'Rules', - }), + text: RULES_BREADCRUMB_TEXT, }, ]); - const rulesTableColumns = [ - { - field: 'name', - name: i18n.translate('xpack.observability.rules.rulesTable.columns.nameTitle', { - defaultMessage: 'Rule Name', - }), - }, - { - field: 'executionStatus.lastExecutionDate', - name: i18n.translate('xpack.observability.rules.rulesTable.columns.lastRunTitle', { - defaultMessage: 'Last run', - }), - render: (date: Date) => { - if (date) { + const getRulesTableColumns = () => { + return [ + { + field: 'name', + name: RULE_COLUMN_TITLE, + sortable: true, + truncateText: true, + width: '30%', + 'data-test-subj': 'rulesTableCell-name', + render: (name: string, rule: RuleTableItem) => , + }, + { + field: 'executionStatus.lastExecutionDate', + name: LAST_RUN_COLUMN_TITLE, + sortable: true, + render: (date: Date) => , + }, + { + field: 'executionStatus.status', + name: LAST_RESPONSE_COLUMN_TITLE, + sortable: true, + truncateText: false, + width: '120px', + 'data-test-subj': 'rulesTableCell-status', + render: (_executionStatus: AlertExecutionStatus, item: RuleTableItem) => ( + + ), + }, + { + field: 'enabled', + name: STATUS_COLUMN_TITLE, + sortable: true, + render: (_enabled: boolean, item: RuleTableItem) => { return ( - <> - - - - {moment(date).fromNow()} - - - - + reload()} + enableRule={async () => await enableRule({ http, id: item.id })} + disableRule={async () => await disableRule({ http, id: item.id })} + muteRule={async () => await muteRule({ http, id: item.id })} + /> ); - } + }, }, - }, - { - field: 'executionStatus.status', - name: i18n.translate('xpack.observability.rules.rulesTable.columns.lastResponseTitle', { - defaultMessage: 'Last response', - }), - }, - { - field: 'enabled', - name: i18n.translate('xpack.observability.rules.rulesTable.columns.statusTitle', { - defaultMessage: 'Status', - }), - render: (_enabled: boolean) => { - return ( - - - - ); + { + name: ACTIONS_COLUMN_TITLE, + width: '10%', + render(item: RuleTableItem) { + return ( + + + + + onRuleEdit(item)} + iconType={'pencil'} + aria-label={EDIT_ACTION_ARIA_LABEL} + /> + + + setRulesToDelete([item.id])} + iconType={'trash'} + aria-label={DELETE_ACTION_ARIA_LABEL} + /> + + + + + ); + }, }, - }, - { - field: '*', - name: i18n.translate('xpack.observability.rules.rulesTable.columns.actionsTitle', { - defaultMessage: 'Actions', - }), - actions: [ - { - name: 'Edit', - isPrimary: true, - description: 'Edit this rule', - icon: 'pencil', - type: 'icon', - onClick: () => {}, - 'data-test-subj': 'action-edit', + ]; + }; + + const CreateRuleFlyout = useMemo( + () => + triggersActionsUi.getAddAlertFlyout({ + consumer: ALERTS_FEATURE_ID, + onClose: () => { + setCreateRuleFlyoutVisibility(false); + reload(); }, - ], - }, - ]; + filteredSolutions: OBSERVABILITY_SOLUTIONS, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + return ( {i18n.translate('xpack.observability.rulesTitle', { defaultMessage: 'Rules' })} - ), + pageTitle: <>{RULES_PAGE_TITLE} , rightSideItems: [ + setCreateRuleFlyoutVisibility(true)} + > + + , + { + // a new state that rule is deleted, that's the one + setRulesToDelete([]); + // this should cause the fetcher to reload the rules + reload(); + }} + onErrors={async () => { + // Refresh the rules from the server, some rules may have beend deleted + reload(); + setRulesToDelete([]); + }} + onCancel={() => { + setRulesToDelete([]); + }} + apiDeleteCall={deleteRules} + idsToDelete={rulesToDelete} + singleTitle={RULES_SINGLE_TITLE} + multipleTitle={RULES_PLURAL_TITLE} + setIsLoadingState={(isLoading: boolean) => { + setRulesState({ ...rulesState, isLoading }); + }} + /> + + + setRuleLastResponseFilter(ids)} + /> + + @@ -219,8 +265,8 @@ export function RulesPage() { id="xpack.observability.rules.totalItemsCountDescription" defaultMessage="Showing: {pageSize} of {totalItemCount} Rules" values={{ - totalItemCount: rules.totalItemsCount, - pageSize: rules.data.length, + totalItemCount, + pageSize: rules.length, }} /> @@ -237,26 +283,26 @@ export function RulesPage() { - { - setPage(changedPage); - }} - selection={{ - selectable: () => true, - onSelectionChange: (selectedItems) => {}, + setPage(index)} + sort={sort} + onSortChange={(changedSort) => { + setSort(changedSort); }} /> + {error && + toasts.addDanger({ + title: error, + })} + {currentRuleToEdit && } + {createRuleFlyoutVisibility && CreateRuleFlyout} ); } diff --git a/x-pack/plugins/observability/public/pages/rules/translations.ts b/x-pack/plugins/observability/public/pages/rules/translations.ts new file mode 100644 index 0000000000000..36fe05232a1b9 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/translations.ts @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const RULE_STATUS_LICENSE_ERROR = i18n.translate( + 'xpack.observability.rules.rulesTable.ruleStatusLicenseError', + { + defaultMessage: 'License Error', + } +); + +export const RULE_STATUS_OK = i18n.translate('xpack.observability.rules.rulesTable.ruleStatusOk', { + defaultMessage: 'Ok', +}); + +export const RULE_STATUS_ACTIVE = i18n.translate( + 'xpack.observability.rules.rulesTable.ruleStatusActive', + { + defaultMessage: 'Active', + } +); + +export const RULE_STATUS_ERROR = i18n.translate( + 'xpack.observability.rules.rulesTable.ruleStatusError', + { + defaultMessage: 'Error', + } +); + +export const RULE_STATUS_PENDING = i18n.translate( + 'xpack.observability.rules.rulesTable.ruleStatusPending', + { + defaultMessage: 'Pending', + } +); + +export const RULE_STATUS_UNKNOWN = i18n.translate( + 'xpack.observability.rules.rulesTable.ruleStatusUnknown', + { + defaultMessage: 'Unknown', + } +); + +export const LAST_RESPONSE_COLUMN_TITLE = i18n.translate( + 'xpack.observability.rules.rulesTable.columns.lastResponseTitle', + { + defaultMessage: 'Last response', + } +); + +export const LAST_RUN_COLUMN_TITLE = i18n.translate( + 'xpack.observability.rules.rulesTable.columns.lastRunTitle', + { + defaultMessage: 'Last run', + } +); + +export const RULE_COLUMN_TITLE = i18n.translate( + 'xpack.observability.rules.rulesTable.columns.nameTitle', + { + defaultMessage: 'Rule', + } +); + +export const STATUS_COLUMN_TITLE = i18n.translate( + 'xpack.observability.rules.rulesTable.columns.statusTitle', + { + defaultMessage: 'Status', + } +); + +export const ACTIONS_COLUMN_TITLE = i18n.translate( + 'xpack.observability.rules.rulesTable.columns.actionsTitle', + { + defaultMessage: 'Actions', + } +); + +export const EDIT_ACTION_ARIA_LABEL = i18n.translate( + 'xpack.observability.rules.rulesTable.columns.editAriaLabel', + { defaultMessage: 'Edit' } +); + +export const EDIT_ACTION_TOOLTIP = i18n.translate( + 'xpack.observability.rules.rulesTable.columns.editButtonTooltip', + { + defaultMessage: 'Edit', + } +); + +export const DELETE_ACTION_TOOLTIP = i18n.translate( + 'xpack.observability.rules.rulesTable.columns.deleteButtonTooltip', + { defaultMessage: 'Delete' } +); + +export const DELETE_ACTION_ARIA_LABEL = i18n.translate( + 'xpack.observability.rules.rulesTable.columns.deleteAriaLabel', + { defaultMessage: 'Delete' } +); + +export const RULES_PAGE_TITLE = i18n.translate('xpack.observability.rulesTitle', { + defaultMessage: 'Rules', +}); + +export const RULES_BREADCRUMB_TEXT = i18n.translate( + 'xpack.observability.breadcrumbs.rulesLinkText', + { + defaultMessage: 'Rules', + } +); + +export const RULES_LOAD_ERROR = i18n.translate('xpack.observability.rules.loadError', { + defaultMessage: 'Unable to load rules', +}); + +export const RULES_SINGLE_TITLE = i18n.translate( + 'xpack.observability.rules.rulesTable.singleTitle', + { + defaultMessage: 'rule', + } +); + +export const RULES_PLURAL_TITLE = i18n.translate( + 'xpack.observability.rules.rulesTable.pluralTitle', + { + defaultMessage: 'rules', + } +); + +export const confirmModalText = ( + numIdsToDelete: number, + singleTitle: string, + multipleTitle: string +) => + i18n.translate('xpack.observability.rules.deleteSelectedIdsConfirmModal.descriptionText', { + defaultMessage: + "You can't recover {numIdsToDelete, plural, one {a deleted {singleTitle}} other {deleted {multipleTitle}}}.", + values: { numIdsToDelete, singleTitle, multipleTitle }, + }); + +export const confirmButtonText = ( + numIdsToDelete: number, + singleTitle: string, + multipleTitle: string +) => + i18n.translate('xpack.observability.rules.deleteSelectedIdsConfirmModal.deleteButtonLabel', { + defaultMessage: + 'Delete {numIdsToDelete, plural, one {{singleTitle}} other {# {multipleTitle}}} ', + values: { numIdsToDelete, singleTitle, multipleTitle }, + }); + +export const cancelButtonText = i18n.translate( + 'xpack.observability.rules.deleteSelectedIdsConfirmModal.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } +); + +export const deleteSuccessText = ( + numSuccesses: number, + singleTitle: string, + multipleTitle: string +) => + i18n.translate('xpack.observability.rules.deleteSelectedIdsSuccessNotification.descriptionText', { + defaultMessage: + 'Deleted {numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}', + values: { numSuccesses, singleTitle, multipleTitle }, + }); + +export const deleteErrorText = (numErrors: number, singleTitle: string, multipleTitle: string) => + i18n.translate('xpack.observability.rules.deleteSelectedIdsErrorNotification.descriptionText', { + defaultMessage: + 'Failed to delete {numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}', + values: { numErrors, singleTitle, multipleTitle }, + }); diff --git a/x-pack/plugins/observability/public/pages/rules/types.ts b/x-pack/plugins/observability/public/pages/rules/types.ts new file mode 100644 index 0000000000000..9d58847b8e0c9 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/types.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiTableSortingType, EuiBasicTableColumn } from '@elastic/eui'; +import { AlertExecutionStatus } from '../../../../alerting/common'; +import { RuleTableItem, Rule } from '../../../../triggers_actions_ui/public'; +export interface StatusProps { + type: RuleStatus; + onClick: () => void; +} + +export enum RuleStatus { + enabled = 'enabled', + disabled = 'disabled', +} + +export type Status = Record< + RuleStatus, + { + color: string; + label: string; + } +>; + +export interface StatusContextProps { + item: RuleTableItem; + onStatusChanged: (status: RuleStatus) => void; + enableRule: (rule: Rule) => Promise; + disableRule: (rule: Rule) => Promise; + muteRule: (rule: Rule) => Promise; +} + +export interface StatusFilterProps { + selectedStatuses: string[]; + onChange?: (selectedRuleStatusesIds: string[]) => void; +} + +export interface ExecutionStatusProps { + executionStatus: AlertExecutionStatus; +} + +export interface LastRunProps { + date: Date; +} + +export interface RuleNameProps { + name: string; + rule: RuleTableItem; +} + +export interface EditFlyoutProps { + currentRule: RuleTableItem; + onSave: () => Promise; +} + +export interface Pagination { + index: number; + size: number; +} + +export interface FetchRulesProps { + ruleLastResponseFilter: string[]; + page: Pagination; + sort: EuiTableSortingType['sort']; +} + +export interface RulesTableProps { + columns: Array>; + rules: RuleTableItem[]; + page: Pagination; + totalItemCount: number; + onPageChange: (changedPage: Pagination) => void; + sort: EuiTableSortingType['sort']; + onSortChange: (changedSort: EuiTableSortingType['sort']) => void; + isLoading: boolean; +} diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index aea27787af080..c9912b0e29438 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -70,6 +70,7 @@ export interface LifecycleAlertServices< ActionGroupIds extends string = never > { alertWithLifecycle: LifecycleAlertService; + getAlertStartedDate: (alertId: string) => string | null; } export type LifecycleRuleExecutor< @@ -167,6 +168,7 @@ export const createLifecycleExecutor = currentAlerts[id] = fields; return alertFactory.create(id); }, + getAlertStartedDate: (alertId: string) => state.trackedAlerts[alertId]?.started ?? null, }; const nextWrappedState = await wrappedExecutor({ diff --git a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts index 5513aaf532522..dbfc5a9460fe2 100644 --- a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts +++ b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts @@ -35,4 +35,5 @@ export const createLifecycleAlertServicesMock = < alertServices: AlertServices ): LifecycleAlertServices => ({ alertWithLifecycle: ({ id }) => alertServices.alertFactory.create(id), + getAlertStartedDate: jest.fn((id: string) => null), }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts index 61e1ebb47464b..a97a13b8aba38 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts @@ -1125,8 +1125,8 @@ describe('get_filter', () => { undefined ); expect(request).toEqual({ - method: 'POST', - path: `/testindex1,testindex2/_eql/search?allow_no_indices=true`, + allow_no_indices: true, + index: ['testindex1', 'testindex2'], body: { size: 100, query: 'process where true', @@ -1171,8 +1171,8 @@ describe('get_filter', () => { 'event.other_category' ); expect(request).toEqual({ - method: 'POST', - path: `/testindex1,testindex2/_eql/search?allow_no_indices=true`, + allow_no_indices: true, + index: ['testindex1', 'testindex2'], body: { event_category_field: 'event.other_category', size: 100, @@ -1222,8 +1222,8 @@ describe('get_filter', () => { undefined ); expect(request).toEqual({ - method: 'POST', - path: `/testindex1,testindex2/_eql/search?allow_no_indices=true`, + allow_no_indices: true, + index: ['testindex1', 'testindex2'], body: { size: 100, query: 'process where true', diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts index 42c10614975eb..e5fd1e8766ef7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts @@ -12,6 +12,10 @@ import type { } from '@kbn/securitysolution-io-ts-list-types'; import { buildExceptionFilter } from '@kbn/securitysolution-list-utils'; import { Filter, EsQueryConfig, DataViewBase, buildEsQuery } from '@kbn/es-query'; +import { + EqlSearchRequest, + QueryDslQueryContainer, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ESBoolQuery } from '../typed_json'; import { Query, Index, TimestampOverrideOrUndefined } from './schemas/common/schemas'; @@ -58,12 +62,6 @@ export const getAllFilters = (filters: Filter[], exceptionFilter: Filter | undef } }; -interface EqlSearchRequest { - method: string; - path: string; - body: object; -} - export const buildEqlSearchRequest = ( query: string, index: string[], @@ -93,8 +91,7 @@ export const buildEqlSearchRequest = ( excludeExceptions: true, chunkSize: 1024, }); - const indexString = index.join(); - const requestFilter: unknown[] = [ + const requestFilter: QueryDslQueryContainer[] = [ { range: { [timestamp]: { @@ -114,9 +111,16 @@ export const buildEqlSearchRequest = ( }, }); } + const fields = [ + { + field: '*', + include_unmapped: true, + }, + ...docFields, + ]; return { - method: 'POST', - path: `/${indexString}/_eql/search?allow_no_indices=true`, + index, + allow_no_indices: true, body: { size, query, @@ -126,13 +130,7 @@ export const buildEqlSearchRequest = ( }, }, event_category_field: eventCategoryOverride, - fields: [ - { - field: '*', - include_unmapped: true, - }, - ...docFields, - ], + fields, }, }; }; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts index ee5e064fb8666..7c8d6dd6138aa 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts @@ -12,7 +12,7 @@ import { PutTrustedAppUpdateRequestSchema, } from './trusted_apps'; import { ConditionEntryField, OperatingSystem } from '@kbn/securitysolution-utils'; -import { ConditionEntry, NewTrustedApp, PutTrustedAppsRequestParams } from '../types'; +import { TrustedAppConditionEntry, NewTrustedApp, PutTrustedAppsRequestParams } from '../types'; describe('When invoking Trusted Apps Schema', () => { describe('for GET List', () => { @@ -94,7 +94,7 @@ describe('When invoking Trusted Apps Schema', () => { }); describe('for POST Create', () => { - const createConditionEntry = (data?: T): ConditionEntry => ({ + const createConditionEntry = (data?: T): TrustedAppConditionEntry => ({ field: ConditionEntryField.PATH, type: 'match', operator: 'included', @@ -378,7 +378,7 @@ describe('When invoking Trusted Apps Schema', () => { }); describe('for PUT Update', () => { - const createConditionEntry = (data?: T): ConditionEntry => ({ + const createConditionEntry = (data?: T): TrustedAppConditionEntry => ({ field: ConditionEntryField.PATH, type: 'match', operator: 'included', diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index 88ac65768e163..a5476159a03ac 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { ConditionEntryField, OperatingSystem } from '@kbn/securitysolution-utils'; -import { ConditionEntry } from '../types'; +import { TrustedAppConditionEntry } from '../types'; import { getDuplicateFields, isValidHash } from '../service/trusted_apps/validations'; export const DeleteTrustedAppsRequestSchema = { @@ -96,7 +96,7 @@ const MacEntrySchema = schema.object({ const entriesSchemaOptions = { minSize: 1, - validate(entries: ConditionEntry[]) { + validate(entries: TrustedAppConditionEntry[]) { return ( getDuplicateFields(entries) .map((field) => `duplicatedEntry.${field}`) diff --git a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts index 2d2c50572a8bc..df9ff68e5ef3a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts @@ -6,7 +6,7 @@ */ import { ConditionEntryField } from '@kbn/securitysolution-utils'; -import { ConditionEntry } from '../../types'; +import { TrustedAppConditionEntry } from '../../types'; const HASH_LENGTHS: readonly number[] = [ 32, // MD5 @@ -18,8 +18,8 @@ const INVALID_CHARACTERS_PATTERN = /[^0-9a-f]/i; export const isValidHash = (value: string) => HASH_LENGTHS.includes(value.length) && !INVALID_CHARACTERS_PATTERN.test(value); -export const getDuplicateFields = (entries: ConditionEntry[]) => { - const groupedFields = new Map(); +export const getDuplicateFields = (entries: TrustedAppConditionEntry[]) => { + const groupedFields = new Map(); entries.forEach((entry) => { // With the move to the Exception Lists api, the server side now validates individual diff --git a/x-pack/plugins/security_solution/common/endpoint/types/exception_list_items.ts b/x-pack/plugins/security_solution/common/endpoint/types/exception_list_items.ts new file mode 100644 index 0000000000000..efdbe42465a5a --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/types/exception_list_items.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConditionEntryField, EntryTypes } from '@kbn/securitysolution-utils'; + +export type ConditionEntriesMap = { + [K in ConditionEntryField]?: T; +}; + +export interface ConditionEntry< + F extends ConditionEntryField = ConditionEntryField, + T extends EntryTypes = EntryTypes +> { + field: F; + type: T; + operator: 'included'; + value: string | string[]; +} diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 9e19cbd6c99c5..cbbf3010ef7b2 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -12,6 +12,7 @@ import { ManifestSchema } from '../schema/manifest'; export * from './actions'; export * from './os'; export * from './trusted_apps'; +export type { ConditionEntriesMap, ConditionEntry } from './exception_list_items'; /** * Supported React-Router state for the Policy Details page diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index 3872df8d10247..ab56d35d79f99 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -19,6 +19,7 @@ import { PutTrustedAppUpdateRequestSchema, GetTrustedAppsSummaryRequestSchema, } from '../schema/trusted_apps'; +import { ConditionEntry } from './exception_list_items'; /** API request params for deleting Trusted App entry */ export type DeleteTrustedAppsRequestParams = TypeOf; @@ -75,17 +76,18 @@ export enum OperatorFieldIds { matches = 'matches', } -export interface ConditionEntry { +export interface TrustedAppConditionEntry + extends ConditionEntry { field: T; type: TrustedAppEntryTypes; operator: 'included'; value: string; } -export type MacosLinuxConditionEntry = ConditionEntry< +export type MacosLinuxConditionEntry = TrustedAppConditionEntry< ConditionEntryField.HASH | ConditionEntryField.PATH >; -export type WindowsConditionEntry = ConditionEntry< +export type WindowsConditionEntry = TrustedAppConditionEntry< ConditionEntryField.HASH | ConditionEntryField.PATH | ConditionEntryField.SIGNER >; diff --git a/x-pack/plugins/security_solution/common/utils/path_placeholder.ts b/x-pack/plugins/security_solution/common/utils/path_placeholder.ts index 328df398dd576..b915c390b2011 100644 --- a/x-pack/plugins/security_solution/common/utils/path_placeholder.ts +++ b/x-pack/plugins/security_solution/common/utils/path_placeholder.ts @@ -4,11 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { - ConditionEntryField, - OperatingSystem, - TrustedAppEntryTypes, -} from '@kbn/securitysolution-utils'; +import { ConditionEntryField, OperatingSystem, EntryTypes } from '@kbn/securitysolution-utils'; export const getPlaceholderText = () => ({ windows: { @@ -28,7 +24,7 @@ export const getPlaceholderTextByOSType = ({ }: { os: OperatingSystem; field: ConditionEntryField; - type: TrustedAppEntryTypes; + type: EntryTypes; }): string | undefined => { if (field === ConditionEntryField.PATH) { if (os === OperatingSystem.WINDOWS) { diff --git a/x-pack/plugins/security_solution/public/common/components/charts/common.tsx b/x-pack/plugins/security_solution/public/common/components/charts/common.tsx index efb03c1218354..85dbd66d0c05e 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/common.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/common.tsx @@ -104,6 +104,7 @@ const theme: PartialTheme = { }; export const useTheme = () => { const isDarkMode = useUiSetting(DEFAULT_DARK_MODE); + // TODO use the EUI charts theme see src/plugins/charts/public/services/theme/README.md const defaultTheme = isDarkMode ? DARK_THEME : LIGHT_THEME; // eslint-disable-next-line react-hooks/exhaustive-deps const themeValue = useMemo(() => mergeWithDefaultTheme(theme, defaultTheme), []); diff --git a/x-pack/plugins/security_solution/public/common/utils/exception_list_items/index.ts b/x-pack/plugins/security_solution/public/common/utils/exception_list_items/index.ts new file mode 100644 index 0000000000000..1dc32749bc6d5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/exception_list_items/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { + conditionEntriesToEntries, + entriesToConditionEntriesMap, + entriesToConditionEntries, +} from './mappers'; diff --git a/x-pack/plugins/security_solution/public/common/utils/exception_list_items/mappers.ts b/x-pack/plugins/security_solution/public/common/utils/exception_list_items/mappers.ts new file mode 100644 index 0000000000000..e04d059a515d4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/exception_list_items/mappers.ts @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EntriesArray, + EntryMatch, + EntryMatchAny, + EntryMatchWildcard, + EntryNested, + NestedEntriesArray, +} from '@kbn/securitysolution-io-ts-list-types'; +import { ConditionEntryField, EntryTypes } from '@kbn/securitysolution-utils'; + +import { ConditionEntriesMap, ConditionEntry } from '../../../../common/endpoint/types'; + +const OPERATOR_VALUE = 'included'; + +const hashType = (hash: string): 'md5' | 'sha256' | 'sha1' | undefined => { + switch (hash.length) { + case 32: + return 'md5'; + case 40: + return 'sha1'; + case 64: + return 'sha256'; + } +}; + +const createEntryMatch = (field: string, value: string): EntryMatch => { + return { field, value, type: 'match', operator: OPERATOR_VALUE }; +}; + +const createEntryMatchAny = (field: string, value: string[]): EntryMatchAny => { + return { field, value, type: 'match_any', operator: OPERATOR_VALUE }; +}; + +const createEntryMatchWildcard = (field: string, value: string): EntryMatchWildcard => { + return { field, value, type: 'wildcard', operator: OPERATOR_VALUE }; +}; + +const createEntryNested = (field: string, entries: NestedEntriesArray): EntryNested => { + return { field, entries, type: 'nested' }; +}; + +function groupHashEntry(conditionEntry: ConditionEntry): EntriesArray { + const entriesArray: EntriesArray = []; + + if (!Array.isArray(conditionEntry.value)) { + const entry = createEntryMatch( + `process.hash.${hashType(conditionEntry.value)}`, + conditionEntry.value.toLowerCase() + ); + entriesArray.push(entry); + return entriesArray; + } + + const hashTypeGroups: { md5: string[]; sha1: string[]; sha256: string[] } = + conditionEntry.value.reduce( + (memo, val) => { + const type = hashType(val); + if (!type) return memo; + + return { + ...memo, + [type]: [...memo[type], val], + }; + }, + { + md5: [], + sha1: [], + sha256: [], + } as { md5: string[]; sha1: string[]; sha256: string[] } + ); + Object.entries(hashTypeGroups).forEach(([type, values]) => { + if (!values.length) { + return; + } + + const entry = createEntryMatchAny(`process.hash.${type}`, values); + entriesArray.push(entry); + }); + + return entriesArray; +} + +function createNestedSignatureEntry( + value: string | string[], + isTrustedApp: boolean = false +): EntryNested { + const subjectNameMatch = Array.isArray(value) + ? createEntryMatchAny('subject_name', value) + : createEntryMatch('subject_name', value); + const nestedEntries: EntryNested['entries'] = []; + if (isTrustedApp) nestedEntries.push(createEntryMatch('trusted', 'true')); + nestedEntries.push(subjectNameMatch); + return createEntryNested('process.Ext.code_signature', nestedEntries); +} + +function createWildcardPathEntry(value: string | string[]): EntryMatchWildcard | EntryMatchAny { + return Array.isArray(value) + ? createEntryMatchAny('process.executable.caseless', value) + : createEntryMatchWildcard('process.executable.caseless', value); +} + +function createPathEntry(value: string | string[]): EntryMatch | EntryMatchAny { + return Array.isArray(value) + ? createEntryMatchAny('process.executable.caseless', value) + : createEntryMatch('process.executable.caseless', value); +} + +export const conditionEntriesToEntries = ( + conditionEntries: ConditionEntry[], + isTrustedApp: boolean = false +): EntriesArray => { + const entriesArray: EntriesArray = []; + + conditionEntries.forEach((conditionEntry) => { + if (conditionEntry.field === ConditionEntryField.HASH) { + groupHashEntry(conditionEntry).forEach((entry) => entriesArray.push(entry)); + } else if (conditionEntry.field === ConditionEntryField.SIGNER) { + const entry = createNestedSignatureEntry(conditionEntry.value, isTrustedApp); + entriesArray.push(entry); + } else if ( + conditionEntry.field === ConditionEntryField.PATH && + conditionEntry.type === 'wildcard' + ) { + const entry = createWildcardPathEntry(conditionEntry.value); + entriesArray.push(entry); + } else { + const entry = createPathEntry(conditionEntry.value); + entriesArray.push(entry); + } + }); + + return entriesArray; +}; + +const createConditionEntry = ( + field: ConditionEntryField, + type: EntryTypes, + value: string | string[] +): ConditionEntry => { + return { field, value, type, operator: OPERATOR_VALUE }; +}; + +export const entriesToConditionEntriesMap = ( + entries: EntriesArray +): ConditionEntriesMap => { + return entries.reduce((memo: ConditionEntriesMap, entry) => { + if (entry.field.startsWith('process.hash') && entry.type === 'match') { + return { + ...memo, + [ConditionEntryField.HASH]: createConditionEntry( + ConditionEntryField.HASH, + entry.type, + entry.value + ), + } as ConditionEntriesMap; + } else if (entry.field.startsWith('process.hash') && entry.type === 'match_any') { + const currentValues = (memo[ConditionEntryField.HASH]?.value as string[]) ?? []; + + return { + ...memo, + [ConditionEntryField.HASH]: createConditionEntry(ConditionEntryField.HASH, entry.type, [ + ...currentValues, + ...entry.value, + ]), + } as ConditionEntriesMap; + } else if ( + entry.field === ConditionEntryField.PATH && + (entry.type === 'match' || entry.type === 'match_any' || entry.type === 'wildcard') + ) { + return { + ...memo, + [ConditionEntryField.PATH]: createConditionEntry( + ConditionEntryField.PATH, + entry.type, + entry.value + ), + } as ConditionEntriesMap; + } else if (entry.field === ConditionEntryField.SIGNER && entry.type === 'nested') { + const subjectNameCondition = entry.entries.find((subEntry): subEntry is EntryMatch => { + return ( + subEntry.field === 'subject_name' && + (subEntry.type === 'match' || subEntry.type === 'match_any') + ); + }); + + if (subjectNameCondition) { + return { + ...memo, + [ConditionEntryField.SIGNER]: createConditionEntry( + ConditionEntryField.SIGNER, + subjectNameCondition.type, + subjectNameCondition.value + ), + } as ConditionEntriesMap; + } + } + + return memo; + }, {} as ConditionEntriesMap); +}; + +export const entriesToConditionEntries = ( + entries: EntriesArray +): ConditionEntry[] => { + const conditionEntriesMap: ConditionEntriesMap = entriesToConditionEntriesMap(entries); + + return Object.values(conditionEntriesMap).reduce((memo, entry) => { + if (!entry) return memo; + return [...memo, entry]; + }, [] as ConditionEntry[]); +}; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx index 87e3b2bb00519..19f3a810f76d4 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx @@ -214,15 +214,15 @@ export const ArtifactListPage = memo( setSelectedItemForDelete(undefined); }, []); - const handleArtifactFlyoutOnClose = useCallback(() => { - setSelectedItemForEdit(undefined); - }, []); - const handleArtifactFlyoutOnSuccess = useCallback(() => { setSelectedItemForEdit(undefined); refetchListData(); }, [refetchListData]); + const handleArtifactFlyoutOnClose = useCallback(() => { + setSelectedItemForEdit(undefined); + }, []); + if (isPageInitializing) { return ; } @@ -255,6 +255,8 @@ export const ArtifactListPage = memo( labels={labels} size={flyoutSize} submitHandler={onFormSubmit} + policies={policiesRequest.data?.items || []} + policiesIsLoading={policiesRequest.isLoading} data-test-subj={getTestId('flyout')} /> )} diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx index 743f71bfead05..ba37f1ab167e2 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx @@ -107,6 +107,8 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { }, mode: 'create', onChange: expect.any(Function), + policies: expect.any(Array), + policiesIsLoading: false, }, expect.anything() ); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.tsx index 63759df8d42cd..ab893b57f16e8 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.tsx @@ -39,6 +39,7 @@ import { useWithArtifactSubmitData } from '../hooks/use_with_artifact_submit_dat import { useIsArtifactAllowedPerPolicyUsage } from '../hooks/use_is_artifact_allowed_per_policy_usage'; import { useIsMounted } from '../../hooks/use_is_mounted'; import { useGetArtifact } from '../../../hooks/artifacts'; +import type { PolicyData } from '../../../../../common/endpoint/types'; export const ARTIFACT_FLYOUT_LABELS = Object.freeze({ flyoutEditTitle: i18n.translate('xpack.securitySolution.artifactListPage.flyoutEditTitle', { @@ -151,6 +152,8 @@ const createFormInitialState = ( export interface ArtifactFlyoutProps { apiClient: ExceptionsListApiClient; FormComponent: React.ComponentType; + policies: PolicyData[]; + policiesIsLoading: boolean; onSuccess(): void; onClose(): void; submitHandler?: ( @@ -175,6 +178,8 @@ export const ArtifactFlyout = memo( ({ apiClient, item, + policies, + policiesIsLoading, FormComponent, onSuccess, onClose, @@ -373,6 +378,8 @@ export const ArtifactFlyout = memo( item={formState.item} error={submitError ?? undefined} mode={formMode} + policies={policies} + policiesIsLoading={policiesIsLoading} /> )} diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/types.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/types.ts index fa63ebb863ce5..8ee4b50ba4671 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/types.ts +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/types.ts @@ -10,6 +10,7 @@ import type { ExceptionListItemSchema, CreateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { PolicyData } from '../../../../common/endpoint/types'; export interface ArtifactListPageUrlParams { page?: number; @@ -30,6 +31,9 @@ export interface ArtifactFormComponentProps { /** Error will be set if the submission of the form to the api results in an API error. Form can use it to provide feedback to the user */ error: HttpFetchError | undefined; + policies: PolicyData[]; + policiesIsLoading: boolean; + /** reports the state of the form data and the current updated item */ onChange(formStatus: ArtifactFormComponentOnChangeCallbackProps): void; } diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/services/blocklists_api_client.ts b/x-pack/plugins/security_solution/public/management/pages/blocklist/services/blocklists_api_client.ts index fa0451d9363ad..9c02729c68273 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/services/blocklists_api_client.ts +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/services/blocklists_api_client.ts @@ -5,22 +5,61 @@ * 2.0. */ +import { + CreateExceptionListItemSchema, + ExceptionListItemSchema, + UpdateExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_BLOCKLISTS_LIST_ID } from '@kbn/securitysolution-list-constants'; + import { HttpStart } from 'kibana/public'; +import { ConditionEntry } from '../../../../../common/endpoint/types'; +import { + conditionEntriesToEntries, + entriesToConditionEntries, +} from '../../../../common/utils/exception_list_items'; import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; import { BLOCKLISTS_LIST_DEFINITION } from '../constants'; +function readTransform(item: ExceptionListItemSchema): ExceptionListItemSchema { + return { + ...item, + entries: entriesToConditionEntries(item.entries) as ExceptionListItemSchema['entries'], + }; +} + +function writeTransform( + item: T +): T { + return { + ...item, + entries: conditionEntriesToEntries(item.entries as ConditionEntry[]), + } as T; +} + /** * Blocklist exceptions Api client class using ExceptionsListApiClient as base class - * It follow the Singleton pattern. + * It follows the Singleton pattern. * Please, use the getInstance method instead of creating a new instance when using this implementation. */ export class BlocklistsApiClient extends ExceptionsListApiClient { constructor(http: HttpStart) { - super(http, ENDPOINT_BLOCKLISTS_LIST_ID, BLOCKLISTS_LIST_DEFINITION); + super( + http, + ENDPOINT_BLOCKLISTS_LIST_ID, + BLOCKLISTS_LIST_DEFINITION, + readTransform, + writeTransform + ); } public static getInstance(http: HttpStart): ExceptionsListApiClient { - return super.getInstance(http, ENDPOINT_BLOCKLISTS_LIST_ID, BLOCKLISTS_LIST_DEFINITION); + return super.getInstance( + http, + ENDPOINT_BLOCKLISTS_LIST_ID, + BLOCKLISTS_LIST_DEFINITION, + readTransform, + writeTransform + ); } } diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/translations.ts b/x-pack/plugins/security_solution/public/management/pages/blocklist/translations.ts new file mode 100644 index 0000000000000..f7e4344cee23c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/translations.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { ConditionEntryField } from '@kbn/securitysolution-utils'; + +export const DETAILS_HEADER = i18n.translate('xpack.securitySolution.blocklists.details.header', { + defaultMessage: 'Details', +}); + +export const DETAILS_HEADER_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.blocklists.details.header.description', + { + defaultMessage: 'Add a blocklist to prevent selected applications from running on your hosts.', + } +); + +export const NAME_LABEL = i18n.translate('xpack.securitySolution.blocklists.name.label', { + defaultMessage: 'Name', +}); + +export const DESCRIPTION_LABEL = i18n.translate( + 'xpack.securitySolution.blocklists.description.label', + { + defaultMessage: 'Description', + } +); + +export const CONDITIONS_HEADER = i18n.translate( + 'xpack.securitySolution.blocklists.conditions.header', + { + defaultMessage: 'Conditions', + } +); + +export const CONDITIONS_HEADER_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.blocklists.conditions.header.description', + { + defaultMessage: + 'Select an operating system and add conditions. Availability of conditions may depend on your chosen OS.', + } +); + +export const SELECT_OS_LABEL = i18n.translate('xpack.securitySolution.blocklists.os.label', { + defaultMessage: 'Select operating system', +}); + +export const FIELD_LABEL = i18n.translate('xpack.securitySolution.blocklists.field.label', { + defaultMessage: 'Field', +}); + +export const OPERATOR_LABEL = i18n.translate('xpack.securitySolution.blocklists.operator.label', { + defaultMessage: 'Operator', +}); + +export const VALUE_LABEL = i18n.translate('xpack.securitySolution.blocklists.value.label', { + defaultMessage: 'Value', +}); + +export const CONDITION_FIELD_TITLE: { [K in ConditionEntryField]: string } = { + [ConditionEntryField.HASH]: i18n.translate('xpack.securitySolution.blocklists.entry.field.hash', { + defaultMessage: 'Hash', + }), + [ConditionEntryField.PATH]: i18n.translate('xpack.securitySolution.blocklists.entry.field.path', { + defaultMessage: 'Path', + }), + [ConditionEntryField.SIGNER]: i18n.translate( + 'xpack.securitySolution.blocklists.entry.field.signature', + { defaultMessage: 'Signature' } + ), +}; + +export const CONDITION_FIELD_DESCRIPTION: { [K in ConditionEntryField]: string } = { + [ConditionEntryField.HASH]: i18n.translate( + 'xpack.securitySolution.blocklists.entry.field.description.hash', + { defaultMessage: 'md5, sha1, or sha256' } + ), + [ConditionEntryField.PATH]: i18n.translate( + 'xpack.securitySolution.blocklists.entry.field.description.path', + { defaultMessage: 'The full path of the application' } + ), + [ConditionEntryField.SIGNER]: i18n.translate( + 'xpack.securitySolution.blocklists.entry.field.description.signature', + { defaultMessage: 'The signer of the application' } + ), +}; + +export const POLICY_SELECT_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.blocklists.policyAssignmentSectionDescription', + { + defaultMessage: + 'Assign this blocklist globally across all policies, or assign it to specific policies.', + } +); + +export const ERRORS = { + NAME_REQUIRED: i18n.translate('xpack.securitySolution.blocklists.errors.name.required', { + defaultMessage: 'Name is required', + }), + VALUE_REQUIRED: i18n.translate('xpack.securitySolution.blocklists.errors.values.required', { + defaultMessage: 'Field entry must have a value', + }), + INVALID_HASH: i18n.translate('xpack.securitySolution.blocklists.errors.values.invalidHash', { + defaultMessage: 'Invalid hash value', + }), + INVALID_PATH: i18n.translate('xpack.securitySolution.blocklists.errors.values.invalidPath', { + defaultMessage: 'Path may be formed incorrectly; verify value', + }), + WILDCARD_PRESENT: i18n.translate( + 'xpack.securitySolution.blocklists.errors.values.wildcardPresent', + { + defaultMessage: "A wildcard in the filename will affect the endpoint's performance", + } + ), +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx index c016c10ad319f..45d76614ddce2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx @@ -7,32 +7,11 @@ import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; + import { useHttp } from '../../../../common/lib/kibana'; import { ArtifactListPage, ArtifactListPageProps } from '../../../components/artifact_list_page'; import { BlocklistsApiClient } from '../services'; - -// FIXME:PT delete this when real component is implemented -const TempDevFormComponent: ArtifactListPageProps['ArtifactFormComponent'] = (props) => { - // For Dev. Delete once we implement this component - // @ts-ignore - if (!window._dev_artifact_form_props) { - // @ts-ignore - window._dev_artifact_form_props = []; - // @ts-ignore - window.console.log(window._dev_artifact_form_props); - } - // @ts-ignore - window._dev_artifact_form_props.push(props); - - return ( -
-
- {props.error ? props.error?.body?.message || props.error : ''} -
- {`TODO: ${props.mode} Form here`} -
- ); -}; +import { BlockListForm } from './components/blocklist_form'; const BLOCKLIST_PAGE_LABELS: ArtifactListPageProps['labels'] = { pageTitle: i18n.translate('xpack.securitySolution.blocklist.pageTitle', { @@ -123,9 +102,10 @@ export const Blocklist = memo(() => { return ( ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx new file mode 100644 index 0000000000000..293ff2a80aec9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx @@ -0,0 +1,490 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useState, useCallback, memo, useEffect, useRef } from 'react'; +import styled from 'styled-components'; +import { + EuiForm, + EuiFormRow, + EuiFieldText, + EuiTextArea, + EuiHorizontalRule, + EuiText, + EuiSpacer, + EuiSuperSelect, + EuiSuperSelectOption, + EuiComboBox, + EuiComboBoxOptionOption, + EuiTitle, +} from '@elastic/eui'; +import { + OperatingSystem, + ConditionEntryField, + isPathValid, + hasSimpleExecutableName, +} from '@kbn/securitysolution-utils'; +import { isOneOfOperator } from '@kbn/securitysolution-list-utils'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { uniq } from 'lodash'; + +import { OS_TITLES } from '../../../../common/translations'; +import { ArtifactFormComponentProps } from '../../../../components/artifact_list_page'; +import { + CONDITIONS_HEADER, + CONDITIONS_HEADER_DESCRIPTION, + CONDITION_FIELD_DESCRIPTION, + CONDITION_FIELD_TITLE, + DESCRIPTION_LABEL, + DETAILS_HEADER, + DETAILS_HEADER_DESCRIPTION, + FIELD_LABEL, + NAME_LABEL, + OPERATOR_LABEL, + POLICY_SELECT_DESCRIPTION, + SELECT_OS_LABEL, + VALUE_LABEL, + ERRORS, +} from '../../translations'; +import { + EffectedPolicySelect, + EffectedPolicySelection, +} from '../../../../components/effected_policy_select'; +import { + GLOBAL_ARTIFACT_TAG, + BY_POLICY_ARTIFACT_TAG_PREFIX, +} from '../../../../../../common/endpoint/service/artifacts/constants'; +import { useLicense } from '../../../../../common/hooks/use_license'; +import { isValidHash } from '../../../../../../common/endpoint/service/trusted_apps/validations'; +import { isArtifactGlobal } from '../../../../../../common/endpoint/service/artifacts'; +import type { PolicyData } from '../../../../../../common/endpoint/types'; + +interface BlocklistEntry { + field: ConditionEntryField; + operator: 'included'; + type: 'match_any'; + value: string[]; +} + +interface ItemValidation { + name?: React.ReactNode[]; + value?: React.ReactNode[]; +} + +function createValidationMessage(message: string): React.ReactNode { + return
{message}
; +} + +function getDropdownDisplay(field: ConditionEntryField): React.ReactNode { + return ( + <> + {CONDITION_FIELD_TITLE[field]} + + {CONDITION_FIELD_DESCRIPTION[field]} + + + ); +} + +function getMarginForGridArea(gridArea: string): string { + switch (gridArea) { + case 'field': + return '0 4px 0 0'; + case 'operator': + return '0 4px 0 4px'; + case 'value': + return '0 0 0 4px'; + } + + return '0'; +} + +function isValid(itemValidation: ItemValidation): boolean { + return !Object.values(itemValidation).some((error) => error.length); +} + +const InputGroup = styled.div` + display: grid; + grid-template-columns: 25% 25% 50%; + grid-template-areas: 'field operator value'; +`; + +const InputItem = styled.div<{ gridArea: string }>` + grid-area: ${({ gridArea }) => gridArea}; + align-self: center; + vertical-align: baseline; + margin: ${({ gridArea }) => getMarginForGridArea(gridArea)}; +`; + +export const BlockListForm = memo( + ({ item, policies, policiesIsLoading, onChange }: ArtifactFormComponentProps) => { + const [visited, setVisited] = useState<{ name: boolean; value: boolean }>({ + name: false, + value: false, + }); + const warningsRef = useRef({}); + const errorsRef = useRef({}); + const [selectedPolicies, setSelectedPolicies] = useState([]); + + // select policies if editing + useEffect(() => { + const policyIds = item.tags?.map((tag) => tag.split(':')[1]) ?? []; + if (!policyIds.length) return; + const policiesData = policies.filter((policy) => policyIds.includes(policy.id)); + setSelectedPolicies(policiesData); + }, [item.tags, policies]); + + const blocklistEntry = useMemo((): BlocklistEntry => { + if (!item.entries.length) { + return { + field: ConditionEntryField.HASH, + operator: 'included', + type: 'match_any', + value: [], + }; + } + return item.entries[0] as BlocklistEntry; + }, [item.entries]); + + const selectedOs = useMemo((): OperatingSystem => { + if (!item?.os_types?.length) { + return OperatingSystem.WINDOWS; + } + + return item.os_types[0] as OperatingSystem; + }, [item?.os_types]); + + const selectedValues = useMemo(() => { + return blocklistEntry.value.map((label) => ({ label })); + }, [blocklistEntry.value]); + + const osOptions: Array> = useMemo( + () => + [OperatingSystem.LINUX, OperatingSystem.MAC, OperatingSystem.WINDOWS].map((os) => ({ + value: os, + inputDisplay: OS_TITLES[os], + })), + [] + ); + + const fieldOptions: Array> = useMemo(() => { + const selectableFields: Array> = [ + ConditionEntryField.HASH, + ConditionEntryField.PATH, + ].map((field) => ({ + value: field, + inputDisplay: CONDITION_FIELD_TITLE[field], + dropdownDisplay: getDropdownDisplay(field), + })); + if (selectedOs === OperatingSystem.WINDOWS) { + selectableFields.push({ + value: ConditionEntryField.SIGNER, + inputDisplay: CONDITION_FIELD_TITLE[ConditionEntryField.SIGNER], + dropdownDisplay: getDropdownDisplay(ConditionEntryField.SIGNER), + }); + } + + return selectableFields; + }, [selectedOs]); + + const validateValues = useCallback((nextItem: ArtifactFormComponentProps['item']) => { + const os = ((nextItem.os_types ?? [])[0] as OperatingSystem) ?? OperatingSystem.WINDOWS; + const { + field = ConditionEntryField.HASH, + type = 'match_any', + value: values = [], + } = (nextItem.entries[0] ?? {}) as BlocklistEntry; + + const newValueWarnings: React.ReactNode[] = []; + const newNameErrors: React.ReactNode[] = []; + const newValueErrors: React.ReactNode[] = []; + + // error if name empty + if (!nextItem.name.trim()) { + newNameErrors.push(createValidationMessage(ERRORS.NAME_REQUIRED)); + } + + // error if no values + if (!values.length) { + newValueErrors.push(createValidationMessage(ERRORS.VALUE_REQUIRED)); + } + + // error if invalid hash + if (field === ConditionEntryField.HASH && values.some((value) => !isValidHash(value))) { + newValueErrors.push(createValidationMessage(ERRORS.INVALID_HASH)); + } + + const isInvalidPath = values.some((value) => !isPathValid({ os, field, type, value })); + + // warn if invalid path + if (field !== ConditionEntryField.HASH && isInvalidPath) { + newValueWarnings.push(createValidationMessage(ERRORS.INVALID_PATH)); + } + + // warn if wildcard + if ( + field !== ConditionEntryField.HASH && + !isInvalidPath && + values.some((value) => !hasSimpleExecutableName({ os, type, value })) + ) { + newValueWarnings.push(createValidationMessage(ERRORS.WILDCARD_PRESENT)); + } + + warningsRef.current = { ...warningsRef, value: newValueWarnings }; + errorsRef.current = { name: newNameErrors, value: newValueErrors }; + }, []); + + const handleOnNameBlur = useCallback(() => { + setVisited((prevVisited) => ({ ...prevVisited, name: true })); + validateValues(item); + }, [item, validateValues]); + + const handleOnValueFocus = useCallback(() => { + setVisited((prevVisited) => ({ ...prevVisited, value: true })); + validateValues(item); + }, [item, validateValues]); + + const handleOnNameChange = useCallback( + (event: React.ChangeEvent) => { + const nextItem = { + ...item, + name: event.target.value, + }; + + validateValues(nextItem); + onChange({ + isValid: isValid(errorsRef.current), + item: nextItem, + }); + }, + [validateValues, onChange, item] + ); + + const handleOnDescriptionChange = useCallback( + (event: React.ChangeEvent) => { + onChange({ + isValid: isValid(errorsRef.current), + item: { + ...item, + description: event.target.value, + }, + }); + }, + [onChange, item] + ); + + const handleOnOsChange = useCallback( + (os: OperatingSystem) => { + const nextItem = { + ...item, + os_types: [os], + entries: [ + { + ...blocklistEntry, + field: + os !== OperatingSystem.WINDOWS && + blocklistEntry.field === ConditionEntryField.SIGNER + ? ConditionEntryField.HASH + : blocklistEntry.field, + }, + ], + }; + + validateValues(nextItem); + onChange({ + isValid: isValid(errorsRef.current), + item: nextItem, + }); + }, + [validateValues, blocklistEntry, onChange, item] + ); + + const handleOnFieldChange = useCallback( + (field: ConditionEntryField) => { + const nextItem = { + ...item, + entries: [{ ...blocklistEntry, field }], + }; + + validateValues(nextItem); + onChange({ + isValid: isValid(errorsRef.current), + item: nextItem, + }); + }, + [validateValues, onChange, item, blocklistEntry] + ); + + // only triggered on remove / clear + const handleOnValueChange = useCallback( + (change: Array>) => { + const value = change.map((option) => option.label); + const nextItem = { + ...item, + entries: [{ ...blocklistEntry, value }], + }; + + validateValues(nextItem); + onChange({ + isValid: isValid(errorsRef.current), + item: nextItem, + }); + }, + [validateValues, onChange, item, blocklistEntry] + ); + + const handleOnValueAdd = useCallback( + (option: string) => { + const splitValues = option.split(',').filter((value) => value.trim()); + const value = uniq([...blocklistEntry.value, ...splitValues]); + + const nextItem = { + ...item, + entries: [{ ...blocklistEntry, value }], + }; + + validateValues(nextItem); + + onChange({ + isValid: isValid(errorsRef.current), + item: nextItem, + }); + }, + [validateValues, onChange, item, blocklistEntry] + ); + + const handleOnPolicyChange = useCallback( + (change: EffectedPolicySelection) => { + const tags = change.isGlobal + ? [GLOBAL_ARTIFACT_TAG] + : change.selected.map((policy) => `${BY_POLICY_ARTIFACT_TAG_PREFIX}${policy.id}`); + + setSelectedPolicies(change.selected); + onChange({ + isValid: isValid(errorsRef.current), + item: { + ...item, + tags, + }, + }); + }, + [onChange, item] + ); + + return ( + + +

{DETAILS_HEADER}

+
+ + +

{DETAILS_HEADER_DESCRIPTION}

+
+ + + + + + + + + + +

{CONDITIONS_HEADER}

+
+ + +

{CONDITIONS_HEADER_DESCRIPTION}

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + <> + + + + + +
+ ); + } +); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/mappers.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/mappers.ts index f440a0a394631..fda0dde4c473a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/mappers.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/mappers.ts @@ -7,32 +7,27 @@ import { CreateExceptionListItemSchema, - EntriesArray, - EntryMatch, - EntryMatchWildcard, - EntryNested, ExceptionListItemSchema, - NestedEntriesArray, OsType, UpdateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { ConditionEntryField, OperatingSystem } from '@kbn/securitysolution-utils'; import { - ConditionEntryField, - OperatingSystem, - TrustedAppEntryTypes, -} from '@kbn/securitysolution-utils'; -import { - ConditionEntry, EffectScope, NewTrustedApp, TrustedApp, + TrustedAppConditionEntry, UpdateTrustedApp, + ConditionEntriesMap, } from '../../../../../common/endpoint/types'; import { tagsToEffectScope } from '../../../../../common/endpoint/service/trusted_apps/mapping'; import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../../../common/endpoint/service/artifacts/constants'; +import { + conditionEntriesToEntries, + entriesToConditionEntriesMap, +} from '../../../../common/utils/exception_list_items'; -type ConditionEntriesMap = { [K in ConditionEntryField]?: ConditionEntry }; type Mapping = { [K in T]: U }; const OS_TYPE_TO_OPERATING_SYSTEM: Mapping = { @@ -47,64 +42,10 @@ const OPERATING_SYSTEM_TO_OS_TYPE: Mapping = { [OperatingSystem.WINDOWS]: 'windows', }; -const OPERATOR_VALUE = 'included'; - const filterUndefined = (list: Array): T[] => { return list.filter((item: T | undefined): item is T => item !== undefined); }; -const createConditionEntry = ( - field: T, - type: TrustedAppEntryTypes, - value: string -): ConditionEntry => { - return { field, value, type, operator: OPERATOR_VALUE }; -}; - -const entriesToConditionEntriesMap = (entries: EntriesArray): ConditionEntriesMap => { - return entries.reduce((result, entry) => { - if (entry.field.startsWith('process.hash') && entry.type === 'match') { - return { - ...result, - [ConditionEntryField.HASH]: createConditionEntry( - ConditionEntryField.HASH, - entry.type, - entry.value - ), - }; - } else if ( - entry.field === 'process.executable.caseless' && - (entry.type === 'match' || entry.type === 'wildcard') - ) { - return { - ...result, - [ConditionEntryField.PATH]: createConditionEntry( - ConditionEntryField.PATH, - entry.type, - entry.value - ), - }; - } else if (entry.field === 'process.Ext.code_signature' && entry.type === 'nested') { - const subjectNameCondition = entry.entries.find((subEntry): subEntry is EntryMatch => { - return subEntry.field === 'subject_name' && subEntry.type === 'match'; - }); - - if (subjectNameCondition) { - return { - ...result, - [ConditionEntryField.SIGNER]: createConditionEntry( - ConditionEntryField.SIGNER, - subjectNameCondition.type, - subjectNameCondition.value - ), - }; - } - } - - return result; - }, {} as ConditionEntriesMap); -}; - /** * Map an ExceptionListItem to a TrustedApp item * @param exceptionListItem @@ -114,7 +55,19 @@ export const exceptionListItemToTrustedApp = ( ): TrustedApp => { if (exceptionListItem.os_types[0]) { const os = osFromExceptionItem(exceptionListItem); - const grouped = entriesToConditionEntriesMap(exceptionListItem.entries); + let groupedWin: ConditionEntriesMap = {}; + let groupedMacLinux: ConditionEntriesMap< + TrustedAppConditionEntry + > = {}; + if (os === OperatingSystem.WINDOWS) { + groupedWin = entriesToConditionEntriesMap( + exceptionListItem.entries + ); + } else { + groupedMacLinux = entriesToConditionEntriesMap< + TrustedAppConditionEntry + >(exceptionListItem.entries); + } return { id: exceptionListItem.item_id, @@ -129,17 +82,19 @@ export const exceptionListItemToTrustedApp = ( ...(os === OperatingSystem.LINUX || os === OperatingSystem.MAC ? { os, - entries: filterUndefined([ - grouped[ConditionEntryField.HASH], - grouped[ConditionEntryField.PATH], + entries: filterUndefined< + TrustedAppConditionEntry + >([ + groupedMacLinux[ConditionEntryField.HASH], + groupedMacLinux[ConditionEntryField.PATH], ]), } : { os, - entries: filterUndefined([ - grouped[ConditionEntryField.HASH], - grouped[ConditionEntryField.PATH], - grouped[ConditionEntryField.SIGNER], + entries: filterUndefined([ + groupedWin[ConditionEntryField.HASH], + groupedWin[ConditionEntryField.PATH], + groupedWin[ConditionEntryField.SIGNER], ]), }), }; @@ -152,29 +107,6 @@ const osFromExceptionItem = (exceptionListItem: ExceptionListItemSchema): Truste return OS_TYPE_TO_OPERATING_SYSTEM[exceptionListItem.os_types[0]]; }; -const hashType = (hash: string): 'md5' | 'sha256' | 'sha1' | undefined => { - switch (hash.length) { - case 32: - return 'md5'; - case 40: - return 'sha1'; - case 64: - return 'sha256'; - } -}; - -const createEntryMatch = (field: string, value: string): EntryMatch => { - return { field, value, type: 'match', operator: OPERATOR_VALUE }; -}; - -const createEntryMatchWildcard = (field: string, value: string): EntryMatchWildcard => { - return { field, value, type: 'wildcard', operator: OPERATOR_VALUE }; -}; - -const createEntryNested = (field: string, entries: NestedEntriesArray): EntryNested => { - return { field, entries, type: 'nested' }; -}; - const effectScopeToTags = (effectScope: EffectScope) => { if (effectScope.type === 'policy') { return effectScope.policies.map((policy) => `${BY_POLICY_ARTIFACT_TAG_PREFIX}${policy}`); @@ -183,29 +115,6 @@ const effectScopeToTags = (effectScope: EffectScope) => { } }; -const conditionEntriesToEntries = (conditionEntries: ConditionEntry[]): EntriesArray => { - return conditionEntries.map((conditionEntry) => { - if (conditionEntry.field === ConditionEntryField.HASH) { - return createEntryMatch( - `process.hash.${hashType(conditionEntry.value)}`, - conditionEntry.value.toLowerCase() - ); - } else if (conditionEntry.field === ConditionEntryField.SIGNER) { - return createEntryNested(`process.Ext.code_signature`, [ - createEntryMatch('trusted', 'true'), - createEntryMatch('subject_name', conditionEntry.value), - ]); - } else if ( - conditionEntry.field === ConditionEntryField.PATH && - conditionEntry.type === 'wildcard' - ) { - return createEntryMatchWildcard(`process.executable.caseless`, conditionEntry.value); - } else { - return createEntryMatch(`process.executable.caseless`, conditionEntry.value); - } - }); -}; - /** * Map NewTrustedApp to CreateExceptionListItemOptions. */ @@ -219,7 +128,7 @@ export const newTrustedAppToCreateExceptionListItem = ({ return { comments: [], description, - entries: conditionEntriesToEntries(entries), + entries: conditionEntriesToEntries(entries, true), list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, meta: undefined, name, @@ -251,7 +160,7 @@ export const updatedTrustedAppToUpdateExceptionListItem = ( _version: version, name, description, - entries: conditionEntriesToEntries(entries), + entries: conditionEntriesToEntries(entries, true), os_types: [OPERATING_SYSTEM_TO_OS_TYPE[os]], tags: effectScopeToTags(effectScope), diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts index 22aeedca7312c..1a28e6f3bfecf 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts @@ -7,7 +7,7 @@ import { ConditionEntryField } from '@kbn/securitysolution-utils'; import { - ConditionEntry, + TrustedAppConditionEntry, EffectScope, GlobalEffectScope, MacosLinuxConditionEntry, @@ -17,13 +17,13 @@ import { } from '../../../../../common/endpoint/types'; export const isWindowsTrustedAppCondition = ( - condition: ConditionEntry + condition: TrustedAppConditionEntry ): condition is WindowsConditionEntry => { return condition.field === ConditionEntryField.SIGNER || true; }; export const isMacosLinuxTrustedAppCondition = ( - condition: ConditionEntry + condition: TrustedAppConditionEntry ): condition is MacosLinuxConditionEntry => { return condition.field !== ConditionEntryField.SIGNER; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts index 431894274ee00..f0fab98188927 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts @@ -6,13 +6,13 @@ */ import { ConditionEntryField, OperatingSystem } from '@kbn/securitysolution-utils'; -import { ConditionEntry, NewTrustedApp } from '../../../../../common/endpoint/types'; +import { TrustedAppConditionEntry, NewTrustedApp } from '../../../../../common/endpoint/types'; import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../../common/constants'; import { TrustedAppsListPageState } from '../state'; -export const defaultConditionEntry = (): ConditionEntry => ({ +export const defaultConditionEntry = (): TrustedAppConditionEntry => ({ field: ConditionEntryField.HASH, operator: 'included', type: 'match', diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx index 4ea42c896847c..8d20974249cce 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx @@ -9,7 +9,7 @@ import { shallow, mount } from 'enzyme'; import React from 'react'; import { keys } from 'lodash'; import { ConditionEntryField, OperatingSystem } from '@kbn/securitysolution-utils'; -import { ConditionEntry } from '../../../../../../../common/endpoint/types'; +import { TrustedAppConditionEntry } from '../../../../../../../common/endpoint/types'; import { ConditionEntryInput } from '.'; import { EuiSuperSelectProps } from '@elastic/eui'; @@ -18,7 +18,7 @@ let onRemoveMock: jest.Mock; let onChangeMock: jest.Mock; let onVisitedMock: jest.Mock; -const baseEntry: Readonly = { +const baseEntry: Readonly = { field: ConditionEntryField.HASH, type: 'match', operator: 'included', @@ -36,7 +36,7 @@ describe('Condition entry input', () => { subject: string, os: OperatingSystem = OperatingSystem.WINDOWS, isRemoveDisabled: boolean = false, - entry: ConditionEntry = baseEntry + entry: TrustedAppConditionEntry = baseEntry ) => ( void; - onChange: (newEntry: ConditionEntry, oldEntry: ConditionEntry) => void; + onRemove: (entry: TrustedAppConditionEntry) => void; + onChange: (newEntry: TrustedAppConditionEntry, oldEntry: TrustedAppConditionEntry) => void; /** * invoked when at least one field in the entry was visited (triggered when `onBlur` DOM event is dispatched) * For this component, that will be triggered only when the `value` field is visited, since that is the * only one needs user input. */ - onVisited?: (entry: ConditionEntry) => void; + onVisited?: (entry: TrustedAppConditionEntry) => void; 'data-test-subj'?: string; } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_group/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_group/index.tsx index aed69128847f6..19c37498dacfc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_group/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_group/index.tsx @@ -10,7 +10,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiHideFor, EuiSpacer } from '@el import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n-react'; import { OperatingSystem } from '@kbn/securitysolution-utils'; -import { ConditionEntry } from '../../../../../../../common/endpoint/types'; +import { TrustedAppConditionEntry } from '../../../../../../../common/endpoint/types'; import { AndOrBadge } from '../../../../../../common/components/and_or_badge'; import { ConditionEntryInput, ConditionEntryInputProps } from '../condition_entry_input'; import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator'; @@ -45,7 +45,7 @@ const ConditionGroupFlexGroup = styled(EuiFlexGroup)` export interface ConditionGroupProps { os: OperatingSystem; - entries: ConditionEntry[]; + entries: TrustedAppConditionEntry[]; onEntryRemove: ConditionEntryInputProps['onRemove']; onEntryChange: ConditionEntryInputProps['onChange']; onAndClicked: () => void; @@ -82,7 +82,7 @@ export const ConditionGroup = memo( )}
- {(entries as ConditionEntry[]).map((entry, index) => ( + {(entries as TrustedAppConditionEntry[]).map((entry, index) => ( ): ValidationRe }) ); } else { - const duplicated = getDuplicateFields(values.entries as ConditionEntry[]); + const duplicated = getDuplicateFields(values.entries as TrustedAppConditionEntry[]); if (duplicated.length) { isValid = false; duplicated.forEach((field) => { diff --git a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts index 81f9f20182bec..b8afc98b6f1e5 100644 --- a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts +++ b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.ts @@ -31,7 +31,13 @@ export class ExceptionsListApiClient { constructor( private readonly http: HttpStart, public readonly listId: ListId, - private readonly listDefinition: CreateExceptionListSchema + private readonly listDefinition: CreateExceptionListSchema, + private readonly readTransform?: (item: ExceptionListItemSchema) => ExceptionListItemSchema, + private readonly writeTransform?: < + T extends CreateExceptionListItemSchema | UpdateExceptionListItemSchema + >( + item: T + ) => T ) { this.ensureListExists = this.createExceptionList(); } @@ -94,7 +100,11 @@ export class ExceptionsListApiClient { public static getInstance( http: HttpStart, listId: string, - listDefinition: CreateExceptionListSchema + listDefinition: CreateExceptionListSchema, + readTransform?: (item: ExceptionListItemSchema) => ExceptionListItemSchema, + writeTransform?: ( + item: T + ) => T ): ExceptionsListApiClient { if ( !ExceptionsListApiClient.instance.has(listId) || @@ -102,14 +112,20 @@ export class ExceptionsListApiClient { ) { ExceptionsListApiClient.instance.set( listId, - new ExceptionsListApiClient(http, listId, listDefinition) + new ExceptionsListApiClient(http, listId, listDefinition, readTransform, writeTransform) ); } const currentInstance = ExceptionsListApiClient.instance.get(listId); if (currentInstance) { return currentInstance; } else { - return new ExceptionsListApiClient(http, listId, listDefinition); + return new ExceptionsListApiClient( + http, + listId, + listDefinition, + readTransform, + writeTransform + ); } } @@ -159,17 +175,26 @@ export class ExceptionsListApiClient { filter: string; }> = {}): Promise { await this.ensureListExists; - return this.http.get(`${EXCEPTION_LIST_ITEM_URL}/_find`, { - query: { - page, - per_page: perPage, - sort_field: sortField, - sort_order: sortOrder, - list_id: [this.listId], - namespace_type: ['agnostic'], - filter, - }, - }); + const result = await this.http.get( + `${EXCEPTION_LIST_ITEM_URL}/_find`, + { + query: { + page, + per_page: perPage, + sort_field: sortField, + sort_order: sortOrder, + list_id: [this.listId], + namespace_type: ['agnostic'], + filter, + }, + } + ); + + if (this.readTransform) { + result.data = result.data.map(this.readTransform); + } + + return result; } /** @@ -182,13 +207,19 @@ export class ExceptionsListApiClient { } await this.ensureListExists; - return this.http.get(EXCEPTION_LIST_ITEM_URL, { + let result = await this.http.get(EXCEPTION_LIST_ITEM_URL, { query: { id, item_id: itemId, namespace_type: 'agnostic', }, }); + + if (this.readTransform) { + result = this.readTransform(result); + } + + return result; } /** @@ -199,8 +230,14 @@ export class ExceptionsListApiClient { await this.ensureListExists; this.checkIfIsUsingTheRightInstance(exception.list_id); delete exception.meta; + + let transformedException = exception; + if (this.writeTransform) { + transformedException = this.writeTransform(exception); + } + return this.http.post(EXCEPTION_LIST_ITEM_URL, { - body: JSON.stringify(exception), + body: JSON.stringify(transformedException), }); } @@ -210,8 +247,16 @@ export class ExceptionsListApiClient { */ async update(exception: UpdateExceptionListItemSchema): Promise { await this.ensureListExists; + + let transformedException = exception; + if (this.writeTransform) { + transformedException = this.writeTransform(exception); + } + return this.http.put(EXCEPTION_LIST_ITEM_URL, { - body: JSON.stringify(ExceptionsListApiClient.cleanExceptionsBeforeUpdate(exception)), + body: JSON.stringify( + ExceptionsListApiClient.cleanExceptionsBeforeUpdate(transformedException) + ), }); } diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index 7a36e2ef940e5..7521ccbf9df91 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -188,7 +188,7 @@ function getMatcherFunction({ os: ExceptionListItemSchema['os_types'][number]; }): TranslatedEntryMatcher { return matchAny - ? field.endsWith('.caseless') + ? field.endsWith('.caseless') && os !== 'linux' ? 'exact_caseless_any' : 'exact_cased_any' : field.endsWith('.caseless') diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts index 0d7ac2cea493a..eba22837cda5a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts @@ -36,9 +36,9 @@ describe('eql_executor', () => { beforeEach(() => { alertServices = alertsMock.createAlertServices(); logger = loggingSystemMock.createLogger(); - alertServices.scopedClusterClient.asCurrentUser.transport.request.mockResolvedValue({ + alertServices.scopedClusterClient.asCurrentUser.eql.search.mockResolvedValue({ hits: { - total: { value: 10 }, + total: { relation: 'eq', value: 10 }, events: [], }, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts index ac44d1d6470a6..79a73e7c07ec0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts @@ -24,10 +24,10 @@ import { BulkCreate, WrapHits, WrapSequences, - EqlSignalSearchResponse, RuleRangeTuple, SearchAfterAndBulkCreateReturnType, SimpleHit, + SignalSource, } from '../types'; import { createSearchAfterReturnType, makeFloatString } from '../utils'; import { ExperimentalFeatures } from '../../../../../common/experimental_features'; @@ -110,16 +110,11 @@ export const eqlExecutor = async ({ ); const eqlSignalSearchStart = performance.now(); - logger.debug( - `EQL query request path: ${request.path}, method: ${request.method}, body: ${JSON.stringify( - request.body - )}` - ); + logger.debug(`EQL query request: ${JSON.stringify(request)}`); - // TODO: fix this later - const response = (await services.scopedClusterClient.asCurrentUser.transport.request( + const response = await services.scopedClusterClient.asCurrentUser.eql.search( request - )) as EqlSignalSearchResponse; + ); const eqlSignalSearchEnd = performance.now(); const eqlSearchDuration = makeFloatString(eqlSignalSearchEnd - eqlSignalSearchStart); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 37ed4a78a61a6..a5803dc354040 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -21,7 +21,6 @@ import { } from '../../../../../alerting/server'; import { TermAggregationBucket } from '../../types'; import { - EqlSearchResponse, BaseHit, RuleAlertAction, SearchTypes, @@ -188,8 +187,6 @@ export type AlertSourceHit = estypes.SearchHit; export type WrappedSignalHit = BaseHit; export type BaseSignalHit = estypes.SearchHit; -export type EqlSignalSearchResponse = EqlSearchResponse; - export type RuleExecutorOptions = AlertExecutorOptions< RuleParams, AlertTypeState, diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts index fc69153f0b21b..dc539e76e7946 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts @@ -15,7 +15,7 @@ import { CreateExceptionListItemOptions, UpdateExceptionListItemOptions, } from '../../../../../lists/server'; -import { ConditionEntry } from '../../../../common/endpoint/types'; +import { TrustedAppConditionEntry as ConditionEntry } from '../../../../common/endpoint/types'; import { getDuplicateFields, isValidHash, diff --git a/x-pack/plugins/session_view/common/constants.ts b/x-pack/plugins/session_view/common/constants.ts index 4d0d475859d84..55126973fc08c 100644 --- a/x-pack/plugins/session_view/common/constants.ts +++ b/x-pack/plugins/session_view/common/constants.ts @@ -26,3 +26,5 @@ export const ENTRY_SESSION_ENTITY_ID_PROPERTY = 'process.entry_leader.entity_id' // 3. Fewer round trips to the backend! export const PROCESS_EVENTS_PER_PAGE = 1000; export const MOUSE_EVENT_PLACEHOLDER = { stopPropagation: () => undefined } as React.MouseEvent; + +export const DEBOUNCE_TIMEOUT = 500; diff --git a/x-pack/plugins/session_view/public/components/back_to_investigated_alert/index.test.tsx b/x-pack/plugins/session_view/public/components/back_to_investigated_alert/index.test.tsx new file mode 100644 index 0000000000000..a75977535a12f --- /dev/null +++ b/x-pack/plugins/session_view/public/components/back_to_investigated_alert/index.test.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { BUTTON_TEST_ID, BackToInvestigatedAlert } from './index'; + +describe('BackToInvestigatedAlert component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When BackToInvestigatedAlert is mounted', () => { + it('should render basic back button', async () => { + renderResult = mockedContext.render(); + + expect(renderResult.queryByTestId(BUTTON_TEST_ID)).toBeTruthy(); + }); + it('should call onClick function by clicking the badge', async () => { + const onClick = jest.fn(); + renderResult = mockedContext.render(); + + const badgeButton = renderResult.queryByTestId(BUTTON_TEST_ID); + expect(badgeButton).toBeTruthy(); + badgeButton?.click(); + expect(onClick).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/back_to_investigated_alert/index.tsx b/x-pack/plugins/session_view/public/components/back_to_investigated_alert/index.tsx new file mode 100644 index 0000000000000..6252335c29f61 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/back_to_investigated_alert/index.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiBadge } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useStyles } from './styles'; + +interface BackInvestigatedAlertDeps { + isDisplayedAbove?: boolean; + onClick: () => void; +} + +export const BUTTON_TEST_ID = 'sessionView:backToInvestigatedAlert'; + +/** + * Jump back to investigated alert button, should appear + * when user scrolls the investigated event out of view. + */ +export const BackToInvestigatedAlert = ({ + isDisplayedAbove = false, + onClick, +}: BackInvestigatedAlertDeps) => { + const styles = useStyles({ isDisplayedAbove }); + + return ( +
+ + + +
+ ); +}; diff --git a/x-pack/plugins/session_view/public/components/back_to_investigated_alert/styles.ts b/x-pack/plugins/session_view/public/components/back_to_investigated_alert/styles.ts new file mode 100644 index 0000000000000..205f116e01e75 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/back_to_investigated_alert/styles.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useEuiTheme } from '@elastic/eui'; +import { euiLightVars as theme } from '@kbn/ui-theme'; +import { CSSObject } from '@emotion/react'; + +interface StylesDeps { + isDisplayedAbove: boolean; +} + +export const useStyles = ({ isDisplayedAbove }: StylesDeps) => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const { size, colors, font } = euiTheme; + + const buttonBackgroundColor = colors.primary; + + const container: CSSObject = { + position: 'absolute', + height: size.xxxxl, + width: '100%', + pointerEvents: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }; + + const jumpBackBadge: CSSObject = { + position: 'sticky', + pointerEvents: 'auto', + cursor: 'pointer', + fontWeight: font.weight.regular, + }; + + if (isDisplayedAbove) { + container.top = 0; + container.background = `linear-gradient(180deg, ${theme.euiColorLightestShade} 0%, transparent 100%)`; + } else { + container.bottom = 0; + container.background = `linear-gradient(360deg, ${theme.euiColorLightestShade} 0%, transparent 100%)`; + } + + return { + container, + jumpBackBadge, + buttonBackgroundColor, + }; + }, [isDisplayedAbove, euiTheme]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx index ac6807984ba83..71ee20e07d642 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { mockData } from '../../../common/mocks/constants/session_view_process.mock'; +import { Process } from '../../../common/types/process_tree'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; import { ProcessImpl } from './hooks'; import { ProcessTree } from './index'; @@ -15,6 +16,7 @@ describe('ProcessTree component', () => { let render: () => ReturnType; let renderResult: ReturnType; let mockedContext: AppContextTestRender; + const sessionLeader = mockData[0].events[0]; beforeEach(() => { mockedContext = createAppRootMockRenderer(); @@ -24,7 +26,7 @@ describe('ProcessTree component', () => { it('should render given a valid sessionEntityId and data', () => { renderResult = mockedContext.render( true} @@ -38,12 +40,58 @@ describe('ProcessTree component', () => { expect(renderResult.queryAllByTestId('sessionView:processTreeNode')).toBeTruthy(); }); + it('should auto select jumpToEvent when it exists and without selectedProcess', () => { + const jumpToEvent = mockData[0].events[2]; + const onProcessSelected = jest.fn((process: Process | null) => { + expect(process?.id).toBe(jumpToEvent.process.entity_id); + }); + renderResult = mockedContext.render( + true} + hasNextPage={false} + fetchPreviousPage={() => true} + hasPreviousPage={false} + jumpToEvent={jumpToEvent} + onProcessSelected={onProcessSelected} + /> + ); + expect(renderResult.queryByTestId('sessionView:sessionViewProcessTree')).toBeTruthy(); + expect(renderResult.queryAllByTestId('sessionView:processTreeNode')).toBeTruthy(); + + expect(onProcessSelected).toHaveBeenCalled(); + }); + + it('should auto select session leader without selectedProcess', () => { + const onProcessSelected = jest.fn((process: Process | null) => { + expect(process?.id).toBe(sessionLeader.process.entity_id); + }); + renderResult = mockedContext.render( + true} + hasNextPage={false} + fetchPreviousPage={() => true} + hasPreviousPage={false} + onProcessSelected={onProcessSelected} + /> + ); + expect(renderResult.queryByTestId('sessionView:sessionViewProcessTree')).toBeTruthy(); + expect(renderResult.queryAllByTestId('sessionView:processTreeNode')).toBeTruthy(); + + expect(onProcessSelected).toHaveBeenCalled(); + }); + it('should insert a DOM element used to highlight a process when selectedProcess is set', () => { const mockSelectedProcess = new ProcessImpl(mockData[0].events[0].process.entity_id); renderResult = mockedContext.render( true} @@ -69,7 +117,7 @@ describe('ProcessTree component', () => { renderResult.rerender( true} diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.tsx index 171836b510815..652d2209790b0 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree/index.tsx @@ -4,10 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useRef, useEffect, useLayoutEffect, useCallback } from 'react'; +import React, { useState, useRef, useEffect, useLayoutEffect, useCallback } from 'react'; import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { ProcessTreeNode } from '../process_tree_node'; +import { BackToInvestigatedAlert } from '../back_to_investigated_alert'; import { useProcessTree } from './hooks'; import { Process, ProcessEventsPage, ProcessEvent } from '../../../common/types/process_tree'; import { useScroll } from '../../hooks/use_scroll'; @@ -33,7 +34,7 @@ interface ProcessTreeDeps { // currently selected process selectedProcess?: Process | null; - onProcessSelected: (process: Process) => void; + onProcessSelected: (process: Process | null) => void; setSearchResults?: (results: Process[]) => void; } @@ -51,6 +52,8 @@ export const ProcessTree = ({ onProcessSelected, setSearchResults, }: ProcessTreeDeps) => { + const [isInvestigatedEventVisible, setIsInvestigatedEventVisible] = useState(true); + const [isInvestigatedEventAbove, setIsInvestigatedEventAbove] = useState(false); const styles = useStyles(); const { sessionLeader, processMap, searchResults } = useProcessTree({ @@ -62,6 +65,23 @@ export const ProcessTree = ({ const scrollerRef = useRef(null); const selectionAreaRef = useRef(null); + const onChangeJumpToEventVisibility = useCallback( + (isVisible: boolean, isAbove: boolean) => { + if (isVisible !== isInvestigatedEventVisible) { + setIsInvestigatedEventVisible(isVisible); + } + if (!isVisible && isAbove !== isInvestigatedEventAbove) { + setIsInvestigatedEventAbove(isAbove); + } + }, + [isInvestigatedEventVisible, isInvestigatedEventAbove] + ); + + const handleBackToInvestigatedAlert = useCallback(() => { + onProcessSelected(null); + setIsInvestigatedEventVisible(true); + }, [onProcessSelected]); + useEffect(() => { if (setSearchResults) { setSearchResults(searchResults); @@ -144,35 +164,46 @@ export const ProcessTree = ({ }, [jumpToEvent, processMap, onProcessSelected, selectProcess, selectedProcess, sessionLeader]); return ( -
- {hasPreviousPage && ( - - - - )} - {sessionLeader && ( - - )} + <>
- {hasNextPage && ( - - - + ref={scrollerRef} + css={styles.scroller} + data-test-subj="sessionView:sessionViewProcessTree" + > + {hasPreviousPage && ( + + + + )} + {sessionLeader && ( + + )} +
+ {hasNextPage && ( + + + + )} +
+ {!isInvestigatedEventVisible && ( + )} -
+ ); }; diff --git a/x-pack/plugins/session_view/public/components/process_tree/styles.ts b/x-pack/plugins/session_view/public/components/process_tree/styles.ts index 618285d88be7a..207cc55e49582 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/styles.ts @@ -13,14 +13,15 @@ export const useStyles = () => { const { euiTheme } = useEuiTheme(); const cached = useMemo(() => { - const defaultSelectionColor = euiTheme.colors.primary; + const { colors, font, size } = euiTheme; + const defaultSelectionColor = colors.primary; const scroller: CSSObject = { position: 'relative', - fontFamily: euiTheme.font.familyCode, + fontFamily: font.familyCode, overflow: 'auto', height: '100%', - backgroundColor: euiTheme.colors.lightestShade, + backgroundColor: colors.lightestShade, }; const selectionArea: CSSObject = { @@ -32,10 +33,11 @@ export const useStyles = () => { backgroundColor: defaultSelectionColor, pointerEvents: 'none', opacity: 0.1, + transform: `translateY(-${size.xs})`, }; - const defaultSelected = transparentize(euiTheme.colors.primary, 0.008); - const alertSelected = transparentize(euiTheme.colors.danger, 0.008); + const defaultSelected = transparentize(colors.primary, 0.008); + const alertSelected = transparentize(colors.danger, 0.008); return { scroller, diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts b/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts index ddd4a04d2f991..68928e137f798 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts @@ -16,8 +16,7 @@ export const useStyles = () => { const { size, colors, border } = euiTheme; const container: CSSObject = { - marginTop: size.s, - marginRight: size.s, + margin: `${size.xs} ${size.s} 0 ${size.xs}`, color: colors.text, padding: `${size.s} 0`, borderStyle: 'solid', diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx index 6b85ca1d208e1..de73111053618 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { RefObject } from 'react'; import userEvent from '@testing-library/user-event'; import { processMock, @@ -13,12 +13,30 @@ import { sessionViewAlertProcessMock, } from '../../../common/mocks/constants/session_view_process.mock'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; -import { ProcessTreeNode } from './index'; +import { ProcessDeps, ProcessTreeNode } from './index'; +import { Cancelable } from 'lodash'; +import { DEBOUNCE_TIMEOUT } from '../../../common/constants'; + +jest.useFakeTimers('modern'); describe('ProcessTreeNode component', () => { let render: () => ReturnType; let renderResult: ReturnType; let mockedContext: AppContextTestRender; + const props: ProcessDeps = { + process: processMock, + scrollerRef: { + current: { + getBoundingClientRect: () => ({ + y: 0, + }), + clientHeight: 500, + addEventListener: () => {}, + removeEventListener: () => {}, + }, + } as unknown as RefObject, + onChangeJumpToEventVisibility: jest.fn(), + }; beforeEach(() => { mockedContext = createAppRootMockRenderer(); @@ -26,15 +44,13 @@ describe('ProcessTreeNode component', () => { describe('When ProcessTreeNode is mounted', () => { it('should render given a valid process', async () => { - renderResult = mockedContext.render(); + renderResult = mockedContext.render(); expect(renderResult.queryByTestId('sessionView:processTreeNode')).toBeTruthy(); }); it('should have an alternate rendering for a session leader', async () => { - renderResult = mockedContext.render( - - ); + renderResult = mockedContext.render(); expect(renderResult.container.textContent).toEqual(' bash started by vagrant'); }); @@ -51,7 +67,9 @@ describe('ProcessTreeNode component', () => { hasExec: () => true, }; - renderResult = mockedContext.render(); + renderResult = mockedContext.render( + + ); expect(renderResult.queryByTestId('sessionView:processTreeNodeExecIcon')).toBeTruthy(); expect(renderResult.queryByTestId('sessionView:processTreeNodeExitCode')).toBeTruthy(); @@ -70,10 +88,38 @@ describe('ProcessTreeNode component', () => { }), }; - renderResult = mockedContext.render(); + renderResult = mockedContext.render( + + ); expect(renderResult.queryByTestId('sessionView:processTreeNodeExitCode')).toBeFalsy(); }); + it('calls onChangeJumpToEventVisibility with isVisible false if jumpToEvent is not visible', async () => { + const onChangeJumpToEventVisibility = jest.fn(); + const scrollerRef = { + current: { + ...props.scrollerRef.current, + clientHeight: -500, + addEventListener: (_event: string, scrollFn: (() => void) & Cancelable) => { + scrollFn(); + }, + removeEventListener: (_event: string, _fn: (() => void) & Cancelable) => {}, + }, + } as RefObject; + + renderResult = mockedContext.render( + + ); + + jest.advanceTimersByTime(DEBOUNCE_TIMEOUT); + expect(onChangeJumpToEventVisibility).toHaveBeenCalled(); + }); + it('renders Root Escalation flag properly', async () => { const rootEscalationProcessMock: typeof processMock = { ...processMock, @@ -96,7 +142,9 @@ describe('ProcessTreeNode component', () => { }), }; - renderResult = mockedContext.render(); + renderResult = mockedContext.render( + + ); expect( renderResult.queryByTestId('sessionView:processTreeNodeRootEscalationFlag') @@ -107,7 +155,7 @@ describe('ProcessTreeNode component', () => { const onProcessSelected = jest.fn(); renderResult = mockedContext.render( - + ); userEvent.click(renderResult.getByTestId('sessionView:processTreeNodeRow')); @@ -120,7 +168,7 @@ describe('ProcessTreeNode component', () => { const onProcessSelected = jest.fn(); renderResult = mockedContext.render( - + ); // @ts-ignore @@ -142,14 +190,16 @@ describe('ProcessTreeNode component', () => { ), getAlerts: () => [sessionViewAlertProcessMock.getAlerts()[0]], }; - renderResult = mockedContext.render(); + renderResult = mockedContext.render( + + ); expect(renderResult.queryByTestId('processTreeNodeAlertButton')).toBeTruthy(); expect(renderResult.queryByTestId('processTreeNodeAlertButton')?.textContent).toBe('Alert'); }); it('renders Alerts button when process has more than one alerts', async () => { renderResult = mockedContext.render( - + ); expect(renderResult.queryByTestId('processTreeNodeAlertButton')).toBeTruthy(); @@ -166,7 +216,9 @@ describe('ProcessTreeNode component', () => { (item) => (item = sessionViewAlertProcessMock.getAlerts()[0]) ), }; - renderResult = mockedContext.render(); + renderResult = mockedContext.render( + + ); expect(renderResult.queryByTestId('processTreeNodeAlertButton')).toBeTruthy(); expect(renderResult.queryByTestId('processTreeNodeAlertButton')?.textContent).toBe( @@ -175,7 +227,7 @@ describe('ProcessTreeNode component', () => { }); it('toggle Alert Details button when Alert button is clicked', async () => { renderResult = mockedContext.render( - + ); userEvent.click(renderResult.getByTestId('processTreeNodeAlertButton')); expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeTruthy(); @@ -190,7 +242,9 @@ describe('ProcessTreeNode component', () => { getChildren: () => [childProcessMock], }; - renderResult = mockedContext.render(); + renderResult = mockedContext.render( + + ); expect( renderResult.queryByTestId('sessionView:processTreeNodeChildProcessesButton') @@ -202,7 +256,9 @@ describe('ProcessTreeNode component', () => { getChildren: () => [childProcessMock], }; - renderResult = mockedContext.render(); + renderResult = mockedContext.render( + + ); expect(renderResult.getAllByTestId('sessionView:processTreeNode')).toHaveLength(1); @@ -222,7 +278,7 @@ describe('ProcessTreeNode component', () => { // set a mock search matched indicator for the process (typically done by ProcessTree/helpers.ts) processMock.searchMatched = '/vagrant'; - renderResult = mockedContext.render(); + renderResult = mockedContext.render(); expect( renderResult.getByTestId('sessionView:processNodeSearchHighlight').textContent diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx index f73cc706fd398..2b7a4f8c79469 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx @@ -19,21 +19,27 @@ import React, { MouseEvent, useCallback, useMemo, + RefObject, } from 'react'; import { EuiButton, EuiIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { Process } from '../../../common/types/process_tree'; -import { useStyles } from './styles'; +import { useVisible } from '../../hooks/use_visible'; import { ProcessTreeAlerts } from '../process_tree_alerts'; import { SessionLeaderButton, AlertButton, ChildrenProcessesButton } from './buttons'; import { useButtonStyles } from './use_button_styles'; -interface ProcessDeps { +import { useStyles } from './styles'; + +export interface ProcessDeps { process: Process; isSessionLeader?: boolean; depth?: number; onProcessSelected?: (process: Process) => void; + jumpToEventID?: string; jumpToAlertID?: string; selectedProcessId?: string; + scrollerRef: RefObject; + onChangeJumpToEventVisibility: (isVisible: boolean, isAbove: boolean) => void; } /** @@ -44,8 +50,11 @@ export function ProcessTreeNode({ isSessionLeader = false, depth = 0, onProcessSelected, + jumpToEventID, jumpToAlertID, selectedProcessId, + scrollerRef, + onChangeJumpToEventVisibility, }: ProcessDeps) { const textRef = useRef(null); @@ -71,6 +80,14 @@ export function ProcessTreeNode({ const styles = useStyles({ depth, hasAlerts, hasInvestigatedAlert }); const buttonStyles = useButtonStyles({}); + const nodeRef = useVisible({ + viewPortEl: scrollerRef.current, + visibleCallback: (isVisible, isAbove) => { + onChangeJumpToEventVisibility(isVisible, isAbove); + }, + shouldAddListener: jumpToEventID === process.id, + }); + // Automatically expand alerts list when investigating an alert useEffect(() => { if (hasInvestigatedAlert) { @@ -157,6 +174,7 @@ export function ProcessTreeNode({ key={id + searchMatched} css={styles.processNode} data-test-subj="sessionView:processTreeNode" + ref={nodeRef} > {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
); })} diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts index 8c077b6dfbbd7..bfe247d6ce4b5 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts @@ -39,7 +39,6 @@ export const useStyles = ({ depth, hasAlerts, hasInvestigatedAlert }: StylesDeps marginLeft: size.base, paddingLeft: size.s, borderLeft: border.editable, - marginTop: size.s, }; /** @@ -67,12 +66,10 @@ export const useStyles = ({ depth, hasAlerts, hasInvestigatedAlert }: StylesDeps display: 'block', cursor: 'pointer', position: 'relative', - margin: `${size.s} 0px`, - '&:not(:first-of-type)': { - marginTop: size.s, - }, + padding: `${size.xs} 0px`, '&:hover:before': { backgroundColor: hoverColor, + transform: `translateY(-${size.xs})`, }, '&:before': { position: 'absolute', @@ -83,6 +80,7 @@ export const useStyles = ({ depth, hasAlerts, hasInvestigatedAlert }: StylesDeps borderLeft: `${size.xs} solid ${borderColor}`, backgroundColor: bgColor, width: `calc(100% + ${depth} * ${TREE_INDENT})`, + transform: `translateY(-${size.xs})`, }, }; diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index 9535a604167cc..58899b2bf5c75 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -31,7 +31,7 @@ export const SessionView = ({ sessionEntityId, height, jumpToEvent }: SessionVie const styles = useStyles({ height }); - const onProcessSelected = useCallback((process: Process) => { + const onProcessSelected = useCallback((process: Process | null) => { setSelectedProcess(process); }, []); diff --git a/x-pack/plugins/session_view/public/components/session_view/styles.ts b/x-pack/plugins/session_view/public/components/session_view/styles.ts index d7159ec5b1b39..76348b9d37b6e 100644 --- a/x-pack/plugins/session_view/public/components/session_view/styles.ts +++ b/x-pack/plugins/session_view/public/components/session_view/styles.ts @@ -20,6 +20,7 @@ export const useStyles = ({ height = 500 }: StylesDeps) => { const processTree: CSSObject = { height: `${height}px`, paddingTop: euiTheme.size.s, + position: 'relative', }; const detailPanel: CSSObject = { diff --git a/x-pack/plugins/session_view/public/hooks/use_scroll.ts b/x-pack/plugins/session_view/public/hooks/use_scroll.ts index 716e35dbb0987..7fa5004ff4822 100644 --- a/x-pack/plugins/session_view/public/hooks/use_scroll.ts +++ b/x-pack/plugins/session_view/public/hooks/use_scroll.ts @@ -7,9 +7,9 @@ import { useEffect } from 'react'; import _ from 'lodash'; +import { DEBOUNCE_TIMEOUT } from '../../common/constants'; const SCROLL_END_BUFFER_HEIGHT = 20; -const DEBOUNCE_TIMEOUT = 500; function getScrollPosition(div: HTMLElement) { if (div) { diff --git a/x-pack/plugins/session_view/public/hooks/use_visible.ts b/x-pack/plugins/session_view/public/hooks/use_visible.ts new file mode 100644 index 0000000000000..19f628819fb56 --- /dev/null +++ b/x-pack/plugins/session_view/public/hooks/use_visible.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useRef, MutableRefObject, useEffect } from 'react'; +import { debounce } from 'lodash'; +import { DEBOUNCE_TIMEOUT } from '../../common/constants'; + +interface IUseVisibleDeps { + viewPortEl: HTMLDivElement | null; + visibleCallback: (isVisible: boolean, isAbove: boolean) => void; + shouldAddListener?: boolean; + offset?: number; + debounceTimeout?: number; +} + +/** + * Check if an element is in viewport + + * @param {HTMLDivElement} viewPortEl - View port element of where the scroll takes place + * @param {function} visibleCallback - callback called onScroll, expects (isVisbile: boolean, isAbove: boolean) as params + * @param {boolean} shouldAddListener - if useVisible hook should add the scroll listener + * @param {number} offset - Number of pixels up to the observable element from the top + * @param {number} debounceTimeout - debounce timeout, in ms + */ +export function useVisible({ + viewPortEl, + visibleCallback, + shouldAddListener = false, + offset = 0, + debounceTimeout = DEBOUNCE_TIMEOUT, +}: IUseVisibleDeps): MutableRefObject { + const currentElement = useRef(null); + + const onScroll = debounce(() => { + if (!currentElement.current || !viewPortEl) { + return; + } + + const { height: elHeight, y: elTop } = currentElement.current.getBoundingClientRect(); + const { y: viewPortElTop } = viewPortEl.getBoundingClientRect(); + + const viewPortElBottom = viewPortElTop + viewPortEl.clientHeight; + const elBottom = elTop + elHeight; + const isVisible = elBottom + offset >= viewPortElTop && elTop - offset <= viewPortElBottom; + + // if elBottom + offset < viewPortElTop, the currentElement is above the current scroll window + visibleCallback(isVisible, elBottom + offset < viewPortElTop); + }, debounceTimeout); + + useEffect(() => { + if (shouldAddListener) { + viewPortEl?.addEventListener('scroll', onScroll); + } + return () => { + if (shouldAddListener) { + viewPortEl?.removeEventListener('scroll', onScroll); + } + }; + }); + + return currentElement; +} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index ea47bedb67152..864a4a4fc5ce7 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -9122,7 +9122,6 @@ "xpack.enterpriseSearch.appSearch.metaEngineCreation.successMessage": "Le métamoteur \"{name}\" a été créé", "xpack.enterpriseSearch.appSearch.metaEngineCreation.title": "Créer un métamoteur", "xpack.enterpriseSearch.appSearch.metaEngines.title": "Métamoteurs", - "xpack.enterpriseSearch.appSearch.metaEngines.upgradeDescription": "Veuillez {readDocumentationLink} pour en savoir plus ou effectuer une mise à niveau vers une licence Platinum pour commencer.", "xpack.enterpriseSearch.appSearch.multiInputRows.addValueButtonLabel": "Ajouter une valeur", "xpack.enterpriseSearch.appSearch.multiInputRows.inputRowPlaceholder": "Entrer une valeur", "xpack.enterpriseSearch.appSearch.multiInputRows.removeValueButtonLabel": "Retirer une valeur", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f1d402c1b69b9..49e4f3c10055b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10748,7 +10748,6 @@ "xpack.enterpriseSearch.appSearch.metaEngineCreation.successMessage": "メタエンジン'{name}'が作成されました", "xpack.enterpriseSearch.appSearch.metaEngineCreation.title": "メタエンジンを作成", "xpack.enterpriseSearch.appSearch.metaEngines.title": "メタエンジン", - "xpack.enterpriseSearch.appSearch.metaEngines.upgradeDescription": "詳細またはPlatinumライセンスにアップグレードして開始するには、{readDocumentationLink}。", "xpack.enterpriseSearch.appSearch.multiInputRows.addValueButtonLabel": "値を追加", "xpack.enterpriseSearch.appSearch.multiInputRows.inputRowPlaceholder": "値を入力", "xpack.enterpriseSearch.appSearch.multiInputRows.removeValueButtonLabel": "値を削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a6256f8ff40eb..7a0306c96faf8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10769,7 +10769,6 @@ "xpack.enterpriseSearch.appSearch.metaEngineCreation.successMessage": "元引擎“{name}”已创建", "xpack.enterpriseSearch.appSearch.metaEngineCreation.title": "创建元引擎", "xpack.enterpriseSearch.appSearch.metaEngines.title": "元引擎", - "xpack.enterpriseSearch.appSearch.metaEngines.upgradeDescription": "{readDocumentationLink}以了解更多信息,或升级到白金级许可证以开始。", "xpack.enterpriseSearch.appSearch.multiInputRows.addValueButtonLabel": "添加值", "xpack.enterpriseSearch.appSearch.multiInputRows.inputRowPlaceholder": "输入值", "xpack.enterpriseSearch.appSearch.multiInputRows.removeValueButtonLabel": "删除值", diff --git a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts new file mode 100644 index 0000000000000..fa848d56e0113 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/common/experimental_features.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type ExperimentalFeatures = typeof allowedExperimentalValues; + +/** + * A list of allowed values that can be used in `xpack.triggersActionsUi.enableExperimental`. + * This object is then used to validate and parse the value entered. + */ +export const allowedExperimentalValues = Object.freeze({ + rulesListDatagrid: true, + rulesDetailLogs: false, +}); + +type ExperimentalConfigKeys = Array; +type Mutable = { -readonly [P in keyof T]: T[P] }; + +const TriggersActionsUIInvalidExperimentalValue = class extends Error {}; +const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly; + +/** + * Parses the string value used in `xpack.triggersActionsUi.enableExperimental` kibana configuration, + * which should be a string of values delimited by a comma (`,`) + * + * @param configValue + * @throws TriggersActionsUIInvalidExperimentalValue + */ +export const parseExperimentalConfigValue = (configValue: string[]): ExperimentalFeatures => { + const enabledFeatures: Mutable> = {}; + + for (const value of configValue) { + if (!isValidExperimentalValue(value)) { + throw new TriggersActionsUIInvalidExperimentalValue(`[${value}] is not valid.`); + } + + enabledFeatures[value as keyof ExperimentalFeatures] = true; + } + + return { + ...allowedExperimentalValues, + ...enabledFeatures, + }; +}; + +export const isValidExperimentalValue = (value: string): boolean => { + return allowedKeys.includes(value as keyof ExperimentalFeatures); +}; + +export const getExperimentalAllowedValues = (): string[] => [...allowedKeys]; diff --git a/x-pack/plugins/ml/server/models/memory_overview/index.ts b/x-pack/plugins/triggers_actions_ui/common/types.ts similarity index 75% rename from x-pack/plugins/ml/server/models/memory_overview/index.ts rename to x-pack/plugins/triggers_actions_ui/common/types.ts index 038b1cd8d4b80..c6c767c9bc40f 100644 --- a/x-pack/plugins/ml/server/models/memory_overview/index.ts +++ b/x-pack/plugins/triggers_actions_ui/common/types.ts @@ -5,4 +5,6 @@ * 2.0. */ -export { memoryOverviewServiceProvider } from './memory_overview_service'; +export interface TriggersActionsUiConfigType { + enableExperimental: string[]; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_types.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_types.ts new file mode 100644 index 0000000000000..74f6d9c2197b9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_types.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useEffect, useState, useRef } from 'react'; +import { loadRuleTypes } from '../lib/rule_api'; +import { RuleType, RuleTypeIndex } from '../../types'; +import { useKibana } from '../../common/lib/kibana'; + +interface RuleTypesState { + isLoading: boolean; + data: Array>; + error: string | null; +} + +interface RuleTypesProps { + filteredSolutions?: string[] | undefined; +} + +export function useLoadRuleTypes({ filteredSolutions }: RuleTypesProps) { + const { http } = useKibana().services; + const isMounted = useRef(false); + const [ruleTypesState, setRuleTypesState] = useState({ + isLoading: false, + data: [], + error: null, + }); + const [ruleTypeIndex, setRuleTypeIndex] = useState(new Map()); + + async function fetchRuleTypes() { + setRuleTypesState({ ...ruleTypesState, isLoading: true }); + try { + const response = await loadRuleTypes({ http }); + const index: RuleTypeIndex = new Map(); + for (const ruleTypeItem of response) { + index.set(ruleTypeItem.id, ruleTypeItem); + } + if (isMounted.current) { + setRuleTypeIndex(index); + + let filteredResponse = response; + + if (filteredSolutions && filteredSolutions.length > 0) { + filteredResponse = response.filter((item) => filteredSolutions.includes(item.producer)); + } + setRuleTypesState({ ...ruleTypesState, isLoading: false, data: filteredResponse }); + } + } catch (e) { + if (isMounted.current) { + setRuleTypesState({ ...ruleTypesState, isLoading: false, error: e }); + } + } + } + + useEffect(() => { + isMounted.current = true; + fetchRuleTypes(); + return () => { + isMounted.current = false; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return { + ruleTypes: ruleTypesState.data, + error: ruleTypesState.error, + ruleTypeIndex, + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx index b8c25eef938bb..08fabc3fba2fd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx @@ -105,6 +105,7 @@ export const ExecutionDurationChart: React.FunctionComponent = ({ <> { const onSaveHandler = onSave ?? reloadRules; @@ -252,6 +253,7 @@ const RuleAdd = ({ actionTypeRegistry={actionTypeRegistry} ruleTypeRegistry={ruleTypeRegistry} metadata={metadata} + filteredSolutions={filteredSolutions} /> ({ - loadRuleTypes: jest.fn(), + +jest.mock('../../hooks/use_load_rule_types', () => ({ + useLoadRuleTypes: jest.fn(), })); jest.mock('../../../common/lib/kibana'); @@ -95,7 +96,7 @@ describe('rule_form', () => { async function setup() { const mocks = coreMock.createSetup(); - const { loadRuleTypes } = jest.requireMock('../../lib/rule_api'); + const { useLoadRuleTypes } = jest.requireMock('../../hooks/use_load_rule_types'); const ruleTypes: RuleType[] = [ { id: 'my-rule-type', @@ -144,7 +145,7 @@ describe('rule_form', () => { enabledInLicense: false, }, ]; - loadRuleTypes.mockResolvedValue(ruleTypes); + useLoadRuleTypes.mockReturnValue({ ruleTypes }); const [ { application: { capabilities }, @@ -266,46 +267,48 @@ describe('rule_form', () => { let wrapper: ReactWrapper; async function setup() { - const { loadRuleTypes } = jest.requireMock('../../lib/rule_api'); - - loadRuleTypes.mockResolvedValue([ - { - id: 'other-consumer-producer-rule-type', - name: 'Test', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', + const { useLoadRuleTypes } = jest.requireMock('../../hooks/use_load_rule_types'); + + useLoadRuleTypes.mockReturnValue({ + ruleTypes: [ + { + id: 'other-consumer-producer-rule-type', + name: 'Test', + actionGroups: [ + { + id: 'testActionGroup', + name: 'Test Action Group', + }, + ], + defaultActionGroupId: 'testActionGroup', + minimumLicenseRequired: 'basic', + recoveryActionGroup: RecoveredActionGroup, + producer: ALERTS_FEATURE_ID, + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + test: { read: true, all: true }, }, - ], - defaultActionGroupId: 'testActionGroup', - minimumLicenseRequired: 'basic', - recoveryActionGroup: RecoveredActionGroup, - producer: ALERTS_FEATURE_ID, - authorizedConsumers: { - [ALERTS_FEATURE_ID]: { read: true, all: true }, - test: { read: true, all: true }, }, - }, - { - id: 'same-consumer-producer-rule-type', - name: 'Test', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', + { + id: 'same-consumer-producer-rule-type', + name: 'Test', + actionGroups: [ + { + id: 'testActionGroup', + name: 'Test Action Group', + }, + ], + defaultActionGroupId: 'testActionGroup', + minimumLicenseRequired: 'basic', + recoveryActionGroup: RecoveredActionGroup, + producer: 'test', + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + test: { read: true, all: true }, }, - ], - defaultActionGroupId: 'testActionGroup', - minimumLicenseRequired: 'basic', - recoveryActionGroup: RecoveredActionGroup, - producer: 'test', - authorizedConsumers: { - [ALERTS_FEATURE_ID]: { read: true, all: true }, - test: { read: true, all: true }, }, - }, - ]); + ], + }); const mocks = coreMock.createSetup(); const [ { @@ -379,7 +382,7 @@ describe('rule_form', () => { wrapper.update(); }); - expect(loadRuleTypes).toHaveBeenCalled(); + expect(useLoadRuleTypes).toHaveBeenCalled(); } it('renders rule type options which producer correspond to the rule consumer', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx index 6ec6a93c34af2..9df4679afad26 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx @@ -42,14 +42,12 @@ import { getDurationNumberInItsUnit, getDurationUnitValue, } from '../../../../../alerting/common/parse_duration'; -import { loadRuleTypes } from '../../lib/rule_api'; import { RuleReducerAction, InitialRule } from './rule_reducer'; import { RuleTypeModel, Rule, IErrorObject, RuleAction, - RuleTypeIndex, RuleType, RuleTypeRegistryContract, ActionTypeRegistryContract, @@ -76,6 +74,7 @@ import { ruleTypeCompare, ruleTypeGroupCompare } from '../../lib/rule_type_compa import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; import { SectionLoading } from '../../components/section_loading'; import { DEFAULT_ALERT_INTERVAL } from '../../constants'; +import { useLoadRuleTypes } from '../../hooks/use_load_rule_types'; const ENTER_KEY = 13; @@ -95,6 +94,7 @@ interface RuleFormProps> { setHasActionsDisabled?: (value: boolean) => void; setHasActionsWithBrokenConnector?: (value: boolean) => void; metadata?: MetaData; + filteredSolutions?: string[] | undefined; } const defaultScheduleInterval = getDurationNumberInItsUnit(DEFAULT_ALERT_INTERVAL); @@ -112,9 +112,9 @@ export const RuleForm = ({ ruleTypeRegistry, actionTypeRegistry, metadata, + filteredSolutions, }: RuleFormProps) => { const { - http, notifications: { toasts }, docLinks, application: { capabilities }, @@ -143,7 +143,6 @@ export const RuleForm = ({ rule.throttle ? getDurationUnitValue(rule.throttle) : 'h' ); const [defaultActionGroupId, setDefaultActionGroupId] = useState(undefined); - const [ruleTypeIndex, setRuleTypeIndex] = useState(null); const [availableRuleTypes, setAvailableRuleTypes] = useState< Array<{ ruleTypeModel: RuleTypeModel; ruleType: RuleType }> @@ -156,53 +155,77 @@ export const RuleForm = ({ const [solutions, setSolutions] = useState | undefined>(undefined); const [solutionsFilter, setSolutionFilter] = useState([]); let hasDisabledByLicenseRuleTypes: boolean = false; + const { + ruleTypes, + error: loadRuleTypesError, + ruleTypeIndex, + } = useLoadRuleTypes({ filteredSolutions }); // load rule types useEffect(() => { - (async () => { - try { - const ruleTypesResult = await loadRuleTypes({ http }); - const index: RuleTypeIndex = new Map(); - for (const ruleTypeItem of ruleTypesResult) { - index.set(ruleTypeItem.id, ruleTypeItem); - } - if (rule.ruleTypeId && index.has(rule.ruleTypeId)) { - setDefaultActionGroupId(index.get(rule.ruleTypeId)!.defaultActionGroupId); - } - setRuleTypeIndex(index); - - const availableRuleTypesResult = getAvailableRuleTypes(ruleTypesResult); - setAvailableRuleTypes(availableRuleTypesResult); - - const solutionsResult = availableRuleTypesResult.reduce( - (result: Map, ruleTypeItem) => { - if (!result.has(ruleTypeItem.ruleType.producer)) { - result.set( - ruleTypeItem.ruleType.producer, - (kibanaFeatures - ? getProducerFeatureName(ruleTypeItem.ruleType.producer, kibanaFeatures) - : capitalize(ruleTypeItem.ruleType.producer)) ?? - capitalize(ruleTypeItem.ruleType.producer) - ); + if (rule.ruleTypeId && ruleTypeIndex?.has(rule.ruleTypeId)) { + setDefaultActionGroupId(ruleTypeIndex.get(rule.ruleTypeId)!.defaultActionGroupId); + } + + const getAvailableRuleTypes = (ruleTypesResult: RuleType[]) => + ruleTypeRegistry + .list() + .reduce( + ( + arr: Array<{ ruleType: RuleType; ruleTypeModel: RuleTypeModel }>, + ruleTypeRegistryItem: RuleTypeModel + ) => { + const ruleType = ruleTypesResult.find((item) => ruleTypeRegistryItem.id === item.id); + if (ruleType) { + arr.push({ + ruleType, + ruleTypeModel: ruleTypeRegistryItem, + }); } - return result; + return arr; }, - new Map() - ); - setSolutions( - new Map([...solutionsResult.entries()].sort(([, a], [, b]) => a.localeCompare(b))) + [] + ) + .filter((item) => item.ruleType && hasAllPrivilege(rule, item.ruleType)) + .filter((item) => + rule.consumer === ALERTS_FEATURE_ID + ? !item.ruleTypeModel.requiresAppContext + : item.ruleType!.producer === rule.consumer ); - } catch (e) { - toasts.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.ruleForm.unableToLoadRuleTypesMessage', - { defaultMessage: 'Unable to load rule types' } - ), - }); - } - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + + const availableRuleTypesResult = getAvailableRuleTypes(ruleTypes); + setAvailableRuleTypes(availableRuleTypesResult); + + const solutionsResult = availableRuleTypesResult.reduce( + (result: Map, ruleTypeItem) => { + if (!result.has(ruleTypeItem.ruleType.producer)) { + result.set( + ruleTypeItem.ruleType.producer, + (kibanaFeatures + ? getProducerFeatureName(ruleTypeItem.ruleType.producer, kibanaFeatures) + : capitalize(ruleTypeItem.ruleType.producer)) ?? + capitalize(ruleTypeItem.ruleType.producer) + ); + } + return result; + }, + new Map() + ); + setSolutions( + new Map([...solutionsResult.entries()].sort(([, a], [, b]) => a.localeCompare(b))) + ); + }, [ruleTypes, ruleTypeIndex, rule.ruleTypeId, kibanaFeatures, rule, ruleTypeRegistry]); + + useEffect(() => { + if (loadRuleTypesError) { + toasts.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleForm.unableToLoadRuleTypesMessage', + { defaultMessage: 'Unable to load rule types' } + ), + }); + } + }, [loadRuleTypesError, toasts]); useEffect(() => { setRuleTypeModel(rule.ruleTypeId ? ruleTypeRegistry.get(rule.ruleTypeId) : null); @@ -280,31 +303,6 @@ export const RuleForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [ruleTypeRegistry, availableRuleTypes, searchText, JSON.stringify(solutionsFilter)]); - const getAvailableRuleTypes = (ruleTypesResult: RuleType[]) => - ruleTypeRegistry - .list() - .reduce( - ( - arr: Array<{ ruleType: RuleType; ruleTypeModel: RuleTypeModel }>, - ruleTypeRegistryItem: RuleTypeModel - ) => { - const ruleType = ruleTypesResult.find((item) => ruleTypeRegistryItem.id === item.id); - if (ruleType) { - arr.push({ - ruleType, - ruleTypeModel: ruleTypeRegistryItem, - }); - } - return arr; - }, - [] - ) - .filter((item) => item.ruleType && hasAllPrivilege(rule, item.ruleType)) - .filter((item) => - rule.consumer === ALERTS_FEATURE_ID - ? !item.ruleTypeModel.requiresAppContext - : item.ruleType!.producer === rule.consumer - ); const selectedRuleType = rule?.ruleTypeId ? ruleTypeIndex?.get(rule?.ruleTypeId) : undefined; const recoveryActionGroup = selectedRuleType?.recoveryActionGroup?.id; const getDefaultActionParams = useCallback( diff --git a/x-pack/plugins/triggers_actions_ui/public/common/experimental_features_service.ts b/x-pack/plugins/triggers_actions_ui/public/common/experimental_features_service.ts new file mode 100644 index 0000000000000..563c2e103e76e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/experimental_features_service.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExperimentalFeatures } from '../../common/experimental_features'; + +export class ExperimentalFeaturesService { + private static experimentalFeatures?: ExperimentalFeatures; + + public static init({ experimentalFeatures }: { experimentalFeatures: ExperimentalFeatures }) { + this.experimentalFeatures = experimentalFeatures; + } + + public static get(): ExperimentalFeatures { + if (!this.experimentalFeatures) { + this.throwUninitializedError(); + } + + return this.experimentalFeatures; + } + + private static throwUninitializedError(): never { + throw new Error( + 'Experimental features services not initialized - are you trying to import this module from outside of the triggers actions UI app?' + ); + } +} diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_experimental_features.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_experimental_features.test.tsx new file mode 100644 index 0000000000000..23d77b6e07a9b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_experimental_features.test.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExperimentalFeaturesService } from './experimental_features_service'; +import { getIsExperimentalFeatureEnabled } from './get_experimental_features'; + +describe('getIsExperimentalFeatureEnabled', () => { + it('getIsExperimentalFeatureEnabled returns the flag enablement', async () => { + ExperimentalFeaturesService.init({ + experimentalFeatures: { + rulesListDatagrid: true, + rulesDetailLogs: false, + }, + }); + + let result = getIsExperimentalFeatureEnabled('rulesListDatagrid'); + + expect(result).toEqual(true); + + result = getIsExperimentalFeatureEnabled('rulesDetailLogs'); + + expect(result).toEqual(false); + + expect(() => getIsExperimentalFeatureEnabled('doesNotExist' as any)).toThrowError( + 'Invalid enable value doesNotExist. Allowed values are: rulesListDatagrid, rulesDetailLogs' + ); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_experimental_features.ts b/x-pack/plugins/triggers_actions_ui/public/common/get_experimental_features.ts new file mode 100644 index 0000000000000..706af60579711 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_experimental_features.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ExperimentalFeatures, + allowedExperimentalValues, + isValidExperimentalValue, + getExperimentalAllowedValues, +} from '../../common/experimental_features'; + +const allowedExperimentalValueKeys = getExperimentalAllowedValues(); + +export const getIsExperimentalFeatureEnabled = (feature: keyof ExperimentalFeatures): boolean => { + if (!isValidExperimentalValue(feature)) { + throw new Error( + `Invalid enable value ${feature}. Allowed values are: ${allowedExperimentalValueKeys.join( + ', ' + )}` + ); + } + + return allowedExperimentalValues[feature]; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 3ba665871e721..eb346e43cfbc9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -8,11 +8,14 @@ // TODO: https://github.com/elastic/kibana/issues/110895 /* eslint-disable @kbn/eslint/no_export_all */ +import { PluginInitializerContext } from 'kibana/server'; import { Plugin } from './plugin'; export type { RuleAction, Rule, + RuleType, + RuleTypeIndex, RuleTypeModel, ActionType, ActionTypeRegistryContract, @@ -25,6 +28,7 @@ export type { RuleFlyoutCloseReason, RuleTypeParams, AsApiContract, + RuleTableItem, } from './types'; export { @@ -39,15 +43,20 @@ export { AlertConditions, AlertConditionsGroup } from './application/sections'; export * from './common'; -export function plugin() { - return new Plugin(); +export function plugin(context: PluginInitializerContext) { + return new Plugin(context); } export { Plugin }; export * from './plugin'; // TODO remove this import when we expose the Rules tables as a component export { loadRules } from './application/lib/rule_api/rules'; +export { deleteRules } from './application/lib/rule_api/delete'; +export { enableRule } from './application/lib/rule_api/enable'; +export { disableRule } from './application/lib/rule_api/disable'; +export { muteRule } from './application/lib/rule_api/mute'; export { loadRuleAggregations } from './application/lib/rule_api/aggregate'; +export { useLoadRuleTypes } from './application/hooks/use_load_rule_types'; export { loadActionTypes } from './application/lib/action_connector_api/connector_types'; diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index e11a1d7e61a1d..afeff4ba8364e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -9,6 +9,7 @@ import { CoreSetup, CoreStart, Plugin as CorePlugin } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { ReactElement } from 'react'; +import { PluginInitializerContext } from 'kibana/public'; import { FeaturesPluginStart } from '../../features/public'; import { KibanaFeature } from '../../features/common'; import { registerBuiltInActionTypes } from './application/components/builtin_action_types'; @@ -31,6 +32,11 @@ import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout'; import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout'; import { getAddAlertFlyoutLazy } from './common/get_add_alert_flyout'; import { getEditAlertFlyoutLazy } from './common/get_edit_alert_flyout'; +import { ExperimentalFeaturesService } from './common/experimental_features_service'; +import { + ExperimentalFeatures, + parseExperimentalConfigValue, +} from '../common/experimental_features'; import type { ActionTypeModel, @@ -40,6 +46,7 @@ import type { ConnectorAddFlyoutProps, ConnectorEditFlyoutProps, } from './types'; +import { TriggersActionsUiConfigType } from '../common/types'; export interface TriggersAndActionsUIPublicPluginSetup { actionTypeRegistry: TypeRegistry; @@ -89,16 +96,22 @@ export class Plugin { private actionTypeRegistry: TypeRegistry; private ruleTypeRegistry: TypeRegistry; + private config: TriggersActionsUiConfigType; + readonly experimentalFeatures: ExperimentalFeatures; - constructor() { + constructor(ctx: PluginInitializerContext) { this.actionTypeRegistry = new TypeRegistry(); this.ruleTypeRegistry = new TypeRegistry(); + this.config = ctx.config.get(); + this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental || []); } public setup(core: CoreSetup, plugins: PluginsSetup): TriggersAndActionsUIPublicPluginSetup { const actionTypeRegistry = this.actionTypeRegistry; const ruleTypeRegistry = this.ruleTypeRegistry; + ExperimentalFeaturesService.init({ experimentalFeatures: this.experimentalFeatures }); + const featureTitle = i18n.translate('xpack.triggersActionsUI.managementSection.displayName', { defaultMessage: 'Rules and Connectors', }); diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index d4658c3e3f5a5..2446b76f956ed 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -339,6 +339,7 @@ export interface RuleAddProps> { onSave?: () => Promise; metadata?: MetaData; ruleTypeIndex?: RuleTypeIndex; + filteredSolutions?: string[] | undefined; } export enum Percentiles { diff --git a/x-pack/plugins/triggers_actions_ui/server/config.ts b/x-pack/plugins/triggers_actions_ui/server/config.ts new file mode 100644 index 0000000000000..46593fff21340 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/server/config.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginInitializerContext } from '../../../../src/core/server'; + +import { + ExperimentalFeatures, + getExperimentalAllowedValues, + isValidExperimentalValue, + parseExperimentalConfigValue, +} from '../common/experimental_features'; + +const allowedExperimentalValues = getExperimentalAllowedValues(); + +export const configSchema = schema.object({ + enableGeoTrackingThresholdAlert: schema.maybe(schema.boolean({ defaultValue: false })), + enableExperimental: schema.arrayOf(schema.string(), { + defaultValue: () => [], + validate(list) { + for (const key of list) { + if (!isValidExperimentalValue(key)) { + return `[${key}] is not allowed. Allowed values are: ${allowedExperimentalValues.join( + ', ' + )}`; + } + } + }, + }), +}); + +export type ConfigSchema = TypeOf; + +export type ConfigType = ConfigSchema & { + experimentalFeatures: ExperimentalFeatures; +}; + +export const createConfig = (context: PluginInitializerContext): ConfigType => { + const pluginConfig = context.config.get>(); + const experimentalFeatures = parseExperimentalConfigValue(pluginConfig.enableExperimental); + + return { + ...pluginConfig, + experimentalFeatures, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/server/index.ts b/x-pack/plugins/triggers_actions_ui/server/index.ts index 2f33f3bd77cc0..800eacedab8f3 100644 --- a/x-pack/plugins/triggers_actions_ui/server/index.ts +++ b/x-pack/plugins/triggers_actions_ui/server/index.ts @@ -5,7 +5,7 @@ * 2.0. */ import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server'; -import { configSchema, ConfigSchema } from '../config'; +import { configSchema, ConfigSchema } from './config'; import { TriggersActionsPlugin } from './plugin'; export type { PluginStartContract } from './plugin'; @@ -22,6 +22,7 @@ export { export const config: PluginConfigDescriptor = { exposeToBrowser: { enableGeoTrackingThresholdAlert: true, + enableExperimental: true, }, schema: configSchema, }; diff --git a/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap index c90283a8386f4..96023b32dd2ba 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap @@ -201,16 +201,16 @@ exports[`DonutChart component passes correct props without errors for valid prop "colors": Object { "defaultVizColor": "red", "vizColors": Array [ - "#1EA593", - "#2B70F7", - "#CE0060", - "#38007E", - "#FCA5D3", - "#F37020", - "#E49E29", - "#B0916F", - "#7B000B", - "#34130C", + "#54B399", + "#6092C0", + "#D36086", + "#9170B8", + "#CA8EAE", + "#D6BF57", + "#B9A888", + "#DA8B45", + "#AA6556", + "#E7664C", ], }, "crosshair": Object { @@ -323,8 +323,6 @@ exports[`DonutChart component passes correct props without errors for valid prop "maxColumnWidth": 30, "maxRowHeight": 30, "xAxisLabel": Object { - "align": "center", - "baseline": "middle", "fontFamily": "Sans-Serif", "fontSize": 12, "fontStyle": "normal", @@ -336,12 +334,11 @@ exports[`DonutChart component passes correct props without errors for valid prop "right": 5, "top": 5, }, + "rotation": 0, "textColor": "black", "visible": true, - "width": "auto", }, "yAxisLabel": Object { - "baseline": "middle", "fontFamily": "Sans-Serif", "fontSize": 12, "fontStyle": "normal", diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 11c5af219de4c..197ea15947ab3 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -692,6 +692,18 @@ export default ({ getService }: FtrProviderContext) => { expect(shellSignals.length).eql(100); expect(buildingBlocks.length).eql(200); }); + + it('generates signals when an index name contains special characters to encode', async () => { + const rule: EqlCreateSchema = { + ...getEqlRuleForSignalTesting(['auditbeat-*', '']), + query: 'configuration where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"', + }; + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 1, [id]); + const signals = await getSignalsByIds(supertest, log, [id]); + expect(signals.hits.hits.length).eql(1); + }); }); describe('Threshold Rules', () => { diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts index 0ccbb913f0d10..28f143aa94d52 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts @@ -135,6 +135,7 @@ export default function (providerContext: FtrProviderContext) { expect(body.length).to.be(1); expect(body[0].diff?.length).to.be(2); + expect(body[0].agent_diff?.length).to.be(1); expect(body[0].hasErrors).to.be(false); const [currentPackagePolicy, proposedPackagePolicy] = body[0].diff ?? []; @@ -259,6 +260,7 @@ export default function (providerContext: FtrProviderContext) { expect(body.length).to.be(1); expect(body[0].diff?.length).to.be(2); + expect(body[0].agent_diff?.length).to.be(1); expect(body[0].hasErrors).to.be(false); const [currentPackagePolicy, proposedPackagePolicy] = body[0].diff ?? []; @@ -368,6 +370,7 @@ export default function (providerContext: FtrProviderContext) { expect(body.length).to.be(1); expect(body[0].diff?.length).to.be(2); + expect(body[0].agent_diff?.length).to.be(1); expect(body[0].hasErrors).to.be(false); const [currentPackagePolicy, proposedPackagePolicy] = body[0].diff ?? []; @@ -476,6 +479,7 @@ export default function (providerContext: FtrProviderContext) { expect(body.length).to.be(1); expect(body[0].diff?.length).to.be(2); + expect(body[0].agent_diff?.length).to.be(1); expect(body[0].hasErrors).to.be(false); }); }); diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts index b5a206a43aeb6..3a0e4046291e4 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts @@ -137,7 +137,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('no apm privileges', () => { + // FLAKY: https://github.com/elastic/kibana/issues/122001 + describe.skip('no apm privileges', () => { before(async () => { await security.role.create('no_apm_privileges_role', { elasticsearch: { diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts index 225c4e08be100..2cf7a430bb8c5 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts @@ -144,11 +144,8 @@ export default function ({ getService }: FtrProviderContext) { it('renders Overall swim lane', async () => { await ml.testExecution.logTestStep('has correct axes labels'); - await ml.swimLane.assertAxisLabels(overallSwimLaneTestSubj, 'x', [ - '2016-02-07 00:00', - '2016-02-09 00:00', - '2016-02-11 00:00', - ]); + // The showTimeline prop is set to false and no axis labels are rendered + await ml.swimLane.assertAxisLabels(overallSwimLaneTestSubj, 'x', []); await ml.swimLane.assertAxisLabels(overallSwimLaneTestSubj, 'y', ['Overall']); }); @@ -156,8 +153,11 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('has correct axes labels'); await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'x', [ '2016-02-07 00:00', - '2016-02-09 00:00', - '2016-02-11 00:00', + '2016-02-07 20:00', + '2016-02-08 16:00', + '2016-02-09 12:00', + '2016-02-10 08:00', + '2016-02-11 04:00', ]); await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', [ 'AAL', diff --git a/x-pack/test/functional/services/ml/swim_lane.ts b/x-pack/test/functional/services/ml/swim_lane.ts index ac9568987286b..914e5cc143f3b 100644 --- a/x-pack/test/functional/services/ml/swim_lane.ts +++ b/x-pack/test/functional/services/ml/swim_lane.ts @@ -22,7 +22,7 @@ export function SwimLaneProvider({ getService }: FtrProviderContext) { /** * Y axis labels width + padding */ - const xOffset = 185; + const xOffset = 170; /** * Get coordinates relative to the left top corner of the canvas diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index ae1db94292639..45b52a00eb246 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -190,6 +190,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { policyInfo.agentPolicy.id ); + expect(agentFullPolicy.inputs[0].id).to.eql(policyInfo.packagePolicy.id); expect(agentFullPolicy.inputs[0].policy.linux.advanced.agent.connection_delay).to.eql( 'true' ); diff --git a/yarn.lock b/yarn.lock index ca79fe87e35fa..3fec60123ae73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1431,10 +1431,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@43.1.1": - version "43.1.1" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-43.1.1.tgz#2a9cd4bbde9397b86a45d8aa604a1950ae0997c0" - integrity sha512-lYTdwpARIDXD15iC4cujKplBhGXb3zriBATp0wFsqgT9XE9TMOzlQ9dgylWQ+2x6OlataZLrOMnWXiFQ3uJqqQ== +"@elastic/charts@45.0.1": + version "45.0.1" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-45.0.1.tgz#02362d6345641788f878c99d9943b91923fbb221" + integrity sha512-CrMStOCZzTSO4SxU5FTL75JgG5AshFILVtWqiq2r7+cwwo9oekEPoUuBzqKFCfHkt/yeq1sTdEK19oZsQ0He6w== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -10409,6 +10409,11 @@ commander@2.17.x: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== +commander@7, commander@^7.0.0, commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + commander@^4.0.1, commander@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" @@ -10429,11 +10434,6 @@ commander@^6.1.0, commander@^6.2.1: resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== -commander@^7.0.0, commander@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - commander@^8.2.0: version "8.3.0" resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" @@ -11454,15 +11454,17 @@ cytoscape@^3.10.0: heap "^0.2.6" lodash.debounce "^4.0.8" -d3-array@1, "d3-array@1 - 2", d3-array@1.2.4, d3-array@^1.1.1, d3-array@^1.2.0, d3-array@^1.2.4: +d3-array@1, d3-array@1.2.4, d3-array@^1.1.1, d3-array@^1.2.0, d3-array@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== -d3-array@>=2.5, d3-array@^2.3.0, d3-array@^2.7.1: - version "2.8.0" - resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.8.0.tgz#f76e10ad47f1f4f75f33db5fc322eb9ffde5ef23" - integrity sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw== +"d3-array@1 - 3", "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.1.1.tgz#7797eb53ead6b9083c75a45a681e93fc41bc468c" + integrity sha512-33qQ+ZoZlli19IFiQx4QEpf2CBEayMRzhlisJHSCsSUbDXv6ZishqS1x7uFVClKG4Wr7rZVHvaAttoLow6GqdQ== + dependencies: + internmap "1 - 2" d3-cloud@1.2.5, d3-cloud@^1.2.5: version "1.2.5" @@ -11476,21 +11478,16 @@ d3-collection@1, d3-collection@^1.0.3, d3-collection@^1.0.7: resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e" integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A== -d3-color@1, "d3-color@1 - 2", d3-color@^1.0.3: +d3-color@1, d3-color@^1.0.3: version "1.4.1" resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a" integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q== -"d3-color@1 - 3": +"d3-color@1 - 3", d3-color@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.0.1.tgz#03316e595955d1fcd39d9f3610ad41bb90194d0a" integrity sha512-6/SlHkDOBLyQSJ1j1Ghs82OIUXpKWlR0hCsw0XrLSQhuUPuCSmLQ1QPH98vpnQxMUQM2/gfAkUEWsupVpd9JGw== -d3-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" - integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== - d3-contour@^1.1.0: version "1.3.2" resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3" @@ -11498,14 +11495,14 @@ d3-contour@^1.1.0: dependencies: d3-array "^1.1.1" -d3-delaunay@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-5.3.0.tgz#b47f05c38f854a4e7b3cea80e0bb12e57398772d" - integrity sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w== +d3-delaunay@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.2.tgz#7fd3717ad0eade2fc9939f4260acfb503f984e92" + integrity sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ== dependencies: - delaunator "4" + delaunator "5" -d3-dispatch@1, "d3-dispatch@1 - 2", d3-dispatch@^1.0.3: +d3-dispatch@1, "d3-dispatch@1 - 3", d3-dispatch@^1.0.3: version "1.0.6" resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58" integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA== @@ -11519,13 +11516,13 @@ d3-dsv@^1.2.0: iconv-lite "0.4" rw "1" -d3-dsv@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-2.0.0.tgz#b37b194b6df42da513a120d913ad1be22b5fe7c5" - integrity sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w== +d3-dsv@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73" + integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q== dependencies: - commander "2" - iconv-lite "0.4" + commander "7" + iconv-lite "0.6" rw "1" d3-ease@1: @@ -11533,41 +11530,45 @@ d3-ease@1: resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.6.tgz#ebdb6da22dfac0a22222f2d4da06f66c416a0ec0" integrity sha512-SZ/lVU7LRXafqp7XtIcBdxnWl8yyLpgOmzAk0mWBI9gXNzLDx5ybZgnRbH9dN/yY5tzVBqCQ9avltSnqVwessQ== -d3-force@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-2.1.1.tgz#f20ccbf1e6c9e80add1926f09b51f686a8bc0937" - integrity sha512-nAuHEzBqMvpFVMf9OX75d00OxvOXdxY+xECIXjW6Gv8BRrXu6gAWbv/9XKrvfJ5i5DCokDW7RYE50LRoK092ew== +d3-force@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4" + integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg== dependencies: - d3-dispatch "1 - 2" - d3-quadtree "1 - 2" - d3-timer "1 - 2" + d3-dispatch "1 - 3" + d3-quadtree "1 - 3" + d3-timer "1 - 3" -d3-format@1, "d3-format@1 - 2", d3-format@^1.2.0: +d3-format@1, d3-format@^1.2.0: version "1.4.4" resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.4.tgz#356925f28d0fd7c7983bfad593726fce46844030" integrity sha512-TWks25e7t8/cqctxCmxpUuzZN11QxIA7YrMbram94zMQ0PXjE4LVIMe/f6a4+xxL8HQ3OsAFULOINQi1pE62Aw== -d3-format@^2.0.0: +"d3-format@1 - 3": version "2.0.0" resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== -d3-geo-projection@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-geo-projection/-/d3-geo-projection-3.0.0.tgz#45ad8ce756cdbfa8340b11b2988644d8e1fa42e4" - integrity sha512-1JE+filVbkEX2bT25dJdQ05iA4QHvUwev6o0nIQHOSrNlHCAKfVss/U10vEM3pA4j5v7uQoFdQ4KLbx9BlEbWA== +d3-format@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +d3-geo-projection@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz#dc229e5ead78d31869a4e87cf1f45bd2716c48ca" + integrity sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg== dependencies: - commander "2" - d3-array "1 - 2" - d3-geo "1.12.0 - 2" - resolve "^1.1.10" + commander "7" + d3-array "1 - 3" + d3-geo "1.12.0 - 3" -"d3-geo@1.12.0 - 2", d3-geo@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-2.0.1.tgz#2437fdfed3fe3aba2812bd8f30609cac83a7ee39" - integrity sha512-M6yzGbFRfxzNrVhxDJXzJqSLQ90q1cCyb3EWFZ1LF4eWOBYxFypw7I/NFVBNXKNqxv1bqLathhYvdJ6DC+th3A== +"d3-geo@1.12.0 - 3", d3-geo@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.0.1.tgz#4f92362fd8685d93e3b1fae0fd97dc8980b1ed7e" + integrity sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA== dependencies: - d3-array ">=2.5" + d3-array "2.5.0 - 3" d3-geo@^1.6.4: version "1.12.1" @@ -11581,26 +11582,19 @@ d3-hierarchy@^1.1.4: resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83" integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ== -d3-hierarchy@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz#dab88a58ca3e7a1bc6cab390e89667fcc6d20218" - integrity sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw== +d3-hierarchy@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.1.tgz#9cbb0ffd2375137a351e6cfeed344a06d4ff4597" + integrity sha512-LtAIu54UctRmhGKllleflmHalttH3zkfSi4NlKrTAoFKjC+AFBJohsCAdgCBYQwH0F8hIOGY89X1pPqAchlMkA== -d3-interpolate@1, "d3-interpolate@1.2.0 - 2", d3-interpolate@^1.1.4, d3-interpolate@^1.4.0: +d3-interpolate@1, d3-interpolate@^1.1.4, d3-interpolate@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA== dependencies: d3-color "1" -d3-interpolate@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" - integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== - dependencies: - d3-color "1 - 2" - -d3-interpolate@^3.0.1: +"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== @@ -11612,12 +11606,17 @@ d3-path@1: resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== -"d3-path@1 - 2", d3-path@^2.0.0: +"d3-path@1 - 2", "d3-path@1 - 3": version "2.0.0" resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== -"d3-quadtree@1 - 2": +d3-path@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e" + integrity sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w== + +"d3-quadtree@1 - 3": version "2.0.0" resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-2.0.0.tgz#edbad045cef88701f6fee3aee8e93fb332d30f9d" integrity sha512-b0Ed2t1UUalJpc3qXzKi+cPGxeXRr4KU9YSlocN74aTzp6R/Ud43t79yLLqxHRWZfsvWXmbDWPpoENK1K539xw== @@ -11644,16 +11643,16 @@ d3-scale@1.0.7, d3-scale@^1.0.5, d3-scale@^1.0.7: d3-time "1" d3-time-format "2" -d3-scale@^3.2.2: - version "3.2.3" - resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.2.3.tgz#be380f57f1f61d4ff2e6cbb65a40593a51649cfd" - integrity sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g== +d3-scale@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== dependencies: - d3-array "^2.3.0" - d3-format "1 - 2" - d3-interpolate "1.2.0 - 2" - d3-time "1 - 2" - d3-time-format "2 - 3" + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" d3-selection@^1.0.2, d3-selection@^1.1.0: version "1.4.1" @@ -11674,6 +11673,13 @@ d3-shape@^2.0.0: dependencies: d3-path "1 - 2" +d3-shape@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.1.0.tgz#c8a495652d83ea6f524e482fca57aa3f8bc32556" + integrity sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ== + dependencies: + d3-path "1 - 3" + d3-time-format@2: version "2.2.3" resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.2.3.tgz#0c9a12ee28342b2037e5ea1cf0b9eb4dd75f29cb" @@ -11681,33 +11687,52 @@ d3-time-format@2: dependencies: d3-time "1" -"d3-time-format@2 - 3", d3-time-format@^3.0.0: +"d3-time-format@2 - 4": version "3.0.0" resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag== dependencies: d3-time "1 - 2" +d3-time-format@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + d3-time@1, d3-time@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== -"d3-time@1 - 2", d3-time@^2.0.0: +"d3-time@1 - 2", "d3-time@1 - 3": version "2.0.0" resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.0.0.tgz#ad7c127d17c67bd57a4c61f3eaecb81108b1e0ab" integrity sha512-2mvhstTFcMvwStWd9Tj3e6CEqtOivtD8AUiHT8ido/xmzrI9ijrUUihZ6nHuf/vsScRBonagOdj0Vv+SEL5G3Q== +"d3-time@2.1.1 - 3", d3-time@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.0.0.tgz#65972cb98ae2d4954ef5c932e8704061335d4975" + integrity sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ== + dependencies: + d3-array "2 - 3" + d3-timer@1: version "1.0.10" resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw== -"d3-timer@1 - 2", d3-timer@^2.0.0: +"d3-timer@1 - 3": version "2.0.0" resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-2.0.0.tgz#055edb1d170cfe31ab2da8968deee940b56623e6" integrity sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA== +d3-timer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + d3-transition@^1.0.1: version "1.3.2" resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398" @@ -12139,10 +12164,12 @@ del@^6.0.0: rimraf "^3.0.2" slash "^3.0.0" -delaunator@4: - version "4.0.1" - resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-4.0.1.tgz#3d779687f57919a7a418f8ab947d3bddb6846957" - integrity sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag== +delaunator@5: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b" + integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw== + dependencies: + robust-predicates "^3.0.0" delayed-stream@~1.0.0: version "1.0.0" @@ -16321,7 +16348,7 @@ iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2, iconv-lite@^0.6.3: +iconv-lite@0.6, iconv-lite@^0.6.2, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -16641,6 +16668,11 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + interpret@^1.0.0, interpret@^1.1.0, interpret@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -25053,7 +25085,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.1.10, resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.7.1: +resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.7.1: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -25221,6 +25253,11 @@ robust-predicates@^2.0.4: resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-2.0.4.tgz#0a2367a93abd99676d075981707f29cfb402248b" integrity sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg== +robust-predicates@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.1.tgz#ecde075044f7f30118682bd9fb3f123109577f9a" + integrity sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g== + rollup@^0.25.8: version "0.25.8" resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.25.8.tgz#bf6ce83b87510d163446eeaa577ed6a6fc5835e0" @@ -29036,17 +29073,17 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= -vega-canvas@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/vega-canvas/-/vega-canvas-1.2.5.tgz#c332bad588893391b583fbeb0c95e1143127f797" - integrity sha512-6+jFEd6s7G088npH0+YuN1JTRxMYsjb1uXbwqcJ8zSP7gFR4f3tg/yA/BHU0+JbvLnxnScr61HYpDXAQ59YW3w== +vega-canvas@^1.2.5, vega-canvas@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/vega-canvas/-/vega-canvas-1.2.6.tgz#55e032ce9a62acd17229f6bac66d99db3d6879cd" + integrity sha512-rgeYUpslYn/amIfnuv3Sw6n4BGns94OjjZNtUc9IDji6b+K8LGS/kW+Lvay8JX/oFqtulBp8RLcHN6QjqPLA9Q== -vega-crossfilter@~4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/vega-crossfilter/-/vega-crossfilter-4.0.5.tgz#cf6a5fca60821928f976b32f22cf66cfd9cbeeae" - integrity sha512-yF+iyGP+ZxU7Tcj5yBsMfoUHTCebTALTXIkBNA99RKdaIHp1E690UaGVLZe6xde2n5WaYpho6I/I6wdAW3NXcg== +vega-crossfilter@~4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/vega-crossfilter/-/vega-crossfilter-4.1.0.tgz#b6c5a728ce987f2514074adb22cf86b9bc63e0c8" + integrity sha512-aiOJcvVpiEDIu5uNc4Kf1hakkkPaVOO5fw5T4RSFAw6GEDbdqcB6eZ1xePcsLVic1hxYD5SGiUPdiiIs0SMh2g== dependencies: - d3-array "^2.7.1" + d3-array "^3.1.1" vega-dataflow "^5.7.3" vega-util "^1.15.2" @@ -29059,13 +29096,13 @@ vega-dataflow@^5.7.3, vega-dataflow@^5.7.4, vega-dataflow@~5.7.4: vega-loader "^4.3.2" vega-util "^1.16.1" -vega-encode@~4.8.3: - version "4.8.3" - resolved "https://registry.yarnpkg.com/vega-encode/-/vega-encode-4.8.3.tgz#b3048fb39845d72f18d8dc302ad697f826e0ff83" - integrity sha512-JoRYtaV2Hs8spWLzTu/IjR7J9jqRmuIOEicAaWj6T9NSZrNWQzu2zF3IVsX85WnrIDIRUDaehXaFZvy9uv9RQg== +vega-encode@~4.9.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/vega-encode/-/vega-encode-4.9.0.tgz#3dd1031056bb8029f262afc4b4d58372c8f073a6" + integrity sha512-etv2BHuCn9bzEc0cxyA2TnbtcAFQGVFmsaqmB4sgBCaqTSEfXMoX68LK3yxBrsdm5LU+y3otJVoewi3qWYCx2g== dependencies: - d3-array "^2.7.1" - d3-interpolate "^2.0.1" + d3-array "^3.1.1" + d3-interpolate "^3.0.1" vega-dataflow "^5.7.3" vega-scale "^7.0.3" vega-util "^1.15.2" @@ -29083,63 +29120,63 @@ vega-expression@^5.0.0, vega-expression@~5.0.0: "@types/estree" "^0.0.50" vega-util "^1.16.0" -vega-force@~4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/vega-force/-/vega-force-4.0.7.tgz#6dc39ecb7889d9102661244d62fbc8d8714162ee" - integrity sha512-pyLKdwXSZ9C1dVIqdJOobvBY29rLvZjvRRTla9BU/nMwAiAGlGi6WKUFdRGdneyGe3zo2nSZDTZlZM/Z5VaQNA== +vega-force@~4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/vega-force/-/vega-force-4.1.0.tgz#cc8dea972baa52adc60840ff744ebb9e57d8f1f5" + integrity sha512-Sssf8iH48vYlz+E7/RpU+SUaJbuLoIL87U4tG2Av4gf/hRiImU49x2TI3EuhFWg1zpaCFxlz0CAaX++Oh/gjdw== dependencies: - d3-force "^2.1.1" + d3-force "^3.0.0" vega-dataflow "^5.7.3" vega-util "^1.15.2" -vega-format@^1.0.4, vega-format@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/vega-format/-/vega-format-1.0.4.tgz#40c0c252d11128738b845ee73d8173f8064d6626" - integrity sha512-oTAeub3KWm6nKhXoYCx1q9G3K43R6/pDMXvqDlTSUtjoY7b/Gixm8iLcir5S9bPjvH40n4AcbZsPmNfL/Up77A== +vega-format@^1.0.4, vega-format@^1.1.0, vega-format@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vega-format/-/vega-format-1.1.0.tgz#b9d81cf1bcf222ae5cbd94357ae70245d2c7b18d" + integrity sha512-6mgpeWw8yGdG0Zdi8aVkx5oUrpJGOpNxqazC2858RSDPvChM/jDFlgRMTYw52qk7cxU0L08ARp4BwmXaI75j0w== dependencies: - d3-array "^2.7.1" - d3-format "^2.0.0" - d3-time-format "^3.0.0" + d3-array "^3.1.1" + d3-format "^3.1.0" + d3-time-format "^4.1.0" vega-time "^2.0.3" vega-util "^1.15.2" -vega-functions@^5.10.0, vega-functions@^5.12.1, vega-functions@~5.12.1: - version "5.12.1" - resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.12.1.tgz#b69f9ad4cd9f777dbc942587c02261b2f4cdba2c" - integrity sha512-7cHfcjXOj27qEbh2FTzWDl7FJK4xGcMFF7+oiyqa0fp7BU/wNT5YdNV0t5kCX9WjV7mfJWACKV74usLJbyM6GA== +vega-functions@^5.12.1, vega-functions@^5.13.0, vega-functions@~5.13.0: + version "5.13.0" + resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.13.0.tgz#c9ab8c6eedbf39f75b424cca6776b1d0b8c74b32" + integrity sha512-Mf53zNyx+c9fFqagEI0T8zc9nMlx0zozOngr8oOpG1tZDKOgwOnUgN99zQKbLHjyv+UzWrq3LYTnSLyVe0ZmhQ== dependencies: - d3-array "^2.7.1" - d3-color "^2.0.0" - d3-geo "^2.0.1" + d3-array "^3.1.1" + d3-color "^3.0.1" + d3-geo "^3.0.1" vega-dataflow "^5.7.3" vega-expression "^5.0.0" - vega-scale "^7.1.1" + vega-scale "^7.2.0" vega-scenegraph "^4.9.3" vega-selections "^5.3.1" vega-statistics "^1.7.9" - vega-time "^2.0.4" + vega-time "^2.1.0" vega-util "^1.16.0" -vega-geo@~4.3.8: - version "4.3.8" - resolved "https://registry.yarnpkg.com/vega-geo/-/vega-geo-4.3.8.tgz#5629d18327bb4f3700cdf05db4aced0a43abbf4a" - integrity sha512-fsGxV96Q/QRgPqOPtMBZdI+DneIiROKTG3YDZvGn0EdV16OG5LzFhbNgLT5GPzI+kTwgLpAsucBHklexlB4kfg== +vega-geo@~4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/vega-geo/-/vega-geo-4.4.0.tgz#ce792df57f8ca4c54a7a1a29467cfa24bc53f573" + integrity sha512-3YX41y+J5pu0PMjvBCASg0/lgvu9+QXWJZ+vl6FFKa8AlsIopQ67ZL7ObwqjZcoZMolJ4q0rc+ZO8aj1pXCYcw== dependencies: - d3-array "^2.7.1" - d3-color "^2.0.0" - d3-geo "^2.0.1" + d3-array "^3.1.1" + d3-color "^3.0.1" + d3-geo "^3.0.1" vega-canvas "^1.2.5" vega-dataflow "^5.7.3" vega-projection "^1.4.5" vega-statistics "^1.7.9" vega-util "^1.15.2" -vega-hierarchy@~4.0.9: - version "4.0.9" - resolved "https://registry.yarnpkg.com/vega-hierarchy/-/vega-hierarchy-4.0.9.tgz#4b4bafbc181a14a280ecdbee8874c0db7e369f47" - integrity sha512-4XaWK6V38/QOZ+vllKKTafiwL25m8Kd+ebHmDV+Q236ONHmqc/gv82wwn9nBeXPEfPv4FyJw2SRoqa2Jol6fug== +vega-hierarchy@~4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/vega-hierarchy/-/vega-hierarchy-4.1.0.tgz#605edbe3a6232853f9e8b57e3b905471d33b1a48" + integrity sha512-DWBK39IEt4FiQru12twzKSFUvFFZ7KtlH9+lAaqrJnKuIZFCyQ1XOUfKScfbKIlk4KS+DuCTNLI/pxC/f7Sk9Q== dependencies: - d3-hierarchy "^2.0.0" + d3-hierarchy "^3.1.0" vega-dataflow "^5.7.3" vega-util "^1.15.2" @@ -29148,12 +29185,12 @@ vega-interpreter@^1.0.4: resolved "https://registry.yarnpkg.com/vega-interpreter/-/vega-interpreter-1.0.4.tgz#291ebf85bc2d1c3550a3da22ff75b3ba0d326a39" integrity sha512-6tpYIa/pJz0cZo5fSxDSkZkAA51pID2LjOtQkOQvbzn+sJiCaWKPFhur8MBqbcmYZ9bnap1OYNwlrvpd2qBLvg== -vega-label@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/vega-label/-/vega-label-1.1.0.tgz#0a11ae3ba18d7aed909c51ec67c2a9dde4426c6f" - integrity sha512-LAThIiDEsZxYvbSkvPLJ93eJF+Ts8RXv1IpBh8gmew8XGmaLJvVkzdsMe7WJJwuaVEsK7ZZFyB/Inkp842GW6w== +vega-label@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/vega-label/-/vega-label-1.2.0.tgz#bcb2659aec54f890f9debab3e41ab87a58292dce" + integrity sha512-1prOqkCAfXaUvMqavbGI0nbYGqV8UQR9qvuVwrPJ6Yxm3GIUIOA/JRqNY8eZR8USwMP/kzsqlfVEixj9+Y75VQ== dependencies: - vega-canvas "^1.2.5" + vega-canvas "^1.2.6" vega-dataflow "^5.7.3" vega-scenegraph "^4.9.2" vega-util "^1.15.2" @@ -29175,15 +29212,15 @@ vega-lite@^5.2.0: vega-util "~1.17.0" yargs "~17.2.1" -vega-loader@^4.3.2, vega-loader@^4.3.3, vega-loader@~4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/vega-loader/-/vega-loader-4.4.1.tgz#8f9de46202f33659d1a2737f6e322a9fc3364275" - integrity sha512-dj65i4qlNhK0mOmjuchHgUrF5YUaWrYpx0A8kXA68lBk5Hkx8FNRztkcl07CZJ1+8V81ymEyJii9jzGbhEX0ag== +vega-loader@^4.3.2, vega-loader@^4.4.0, vega-loader@~4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/vega-loader/-/vega-loader-4.5.0.tgz#b15acc4c8f84191f500e94d35ddfb9721ac943ad" + integrity sha512-EkAyzbx0pCYxH3v3wghGVCaKINWxHfgbQ2pYDiYv0yo8e04S8Mv/IlRGTt6BAe7cLhrk1WZ4zh20QOppnGG05w== dependencies: - d3-dsv "^2.0.0" - node-fetch "^2.6.1" + d3-dsv "^3.0.1" + node-fetch "^2.6.7" topojson-client "^3.1.0" - vega-format "^1.0.4" + vega-format "^1.1.0" vega-util "^1.16.0" vega-parser@~6.1.4: @@ -29197,20 +29234,20 @@ vega-parser@~6.1.4: vega-scale "^7.1.1" vega-util "^1.16.0" -vega-projection@^1.4.5, vega-projection@~1.4.5: - version "1.4.5" - resolved "https://registry.yarnpkg.com/vega-projection/-/vega-projection-1.4.5.tgz#020cb646b4eaae535359da25f4f48eef8d324af2" - integrity sha512-85kWcPv0zrrNfxescqHtSYpRknilrS0K3CVRZc7IYQxnLtL1oma9WEbrSr1LCmDoCP5hl2Z1kKbomPXkrQX5Ag== +vega-projection@^1.4.5, vega-projection@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/vega-projection/-/vega-projection-1.5.0.tgz#51c5f0455170cd35b3c5f3e653e99c3616520640" + integrity sha512-aob7qojh555x3hQWZ/tr8cIJNSWQbm6EoWTJaheZgFOY2x3cDa4Qrg3RJbGw6KwVj/IQk2p40paRzixKZ2kr+A== dependencies: - d3-geo "^2.0.1" - d3-geo-projection "^3.0.0" + d3-geo "^3.0.1" + d3-geo-projection "^4.0.0" -vega-regression@~1.0.9: - version "1.0.9" - resolved "https://registry.yarnpkg.com/vega-regression/-/vega-regression-1.0.9.tgz#f33da47fe457e03ad134782c11414bcef7b1da82" - integrity sha512-KSr3QbCF0vJEAWFVY2MA9X786oiJncTTr3gqRMPoaLr/Yo3f7OPKXRoUcw36RiWa0WCOEMgTYtM28iK6ZuSgaA== +vega-regression@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vega-regression/-/vega-regression-1.1.0.tgz#b4394db403ada93de52bb4536d04be336c983a8c" + integrity sha512-09K0RemY6cdaXBAyakDUNFfEkRcLkGjkDJyWQPAUqGK59hV2J+G3i4uxkZp18Vu0t8oqU7CgzwWim1s5uEpOcA== dependencies: - d3-array "^2.7.1" + d3-array "^3.1.1" vega-dataflow "^5.7.3" vega-statistics "^1.7.9" vega-util "^1.15.2" @@ -29223,27 +29260,27 @@ vega-runtime@^6.1.3, vega-runtime@~6.1.3: vega-dataflow "^5.7.3" vega-util "^1.15.2" -vega-scale@^7.0.3, vega-scale@^7.1.1, vega-scale@~7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/vega-scale/-/vega-scale-7.1.1.tgz#b69a38d1980f6fc1093390f796e556be63fdc808" - integrity sha512-yE0to0prA9E5PBJ/XP77TO0BMkzyUVyt7TH5PAwj+CZT7PMsMO6ozihelRhoIiVcP0Ae/ByCEQBUQkzN5zJ0ZA== +vega-scale@^7.0.3, vega-scale@^7.1.1, vega-scale@^7.2.0, vega-scale@~7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/vega-scale/-/vega-scale-7.2.0.tgz#9e298cc02ad340498cb56847436b19439911f0fc" + integrity sha512-QYltO/otrZHLrCGGf06Y99XtPtqWXITr6rw7rO9oL+l3d9o5RFl9sjHrVxiM7v+vGoZVWbBd5IPbFhPsXZ6+TA== dependencies: - d3-array "^2.7.1" - d3-interpolate "^2.0.1" - d3-scale "^3.2.2" - vega-time "^2.0.4" - vega-util "^1.15.2" + d3-array "^3.1.1" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + vega-time "^2.1.0" + vega-util "^1.17.0" -vega-scenegraph@^4.9.2, vega-scenegraph@^4.9.3, vega-scenegraph@^4.9.4, vega-scenegraph@~4.9.4: - version "4.9.4" - resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.4.tgz#468408c1e89703fa9d3450445daabff623de2757" - integrity sha512-QaegQzbFE2yhYLNWAmHwAuguW3yTtQrmwvfxYT8tk0g+KKodrQ5WSmNrphWXhqwtsgVSvtdZkfp2IPeumcOQJg== +vega-scenegraph@^4.10.0, vega-scenegraph@^4.9.2, vega-scenegraph@^4.9.3, vega-scenegraph@~4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.10.0.tgz#232643372760ea081f2a899f640530777c2e2ba8" + integrity sha512-znUQAulNJnuXSza8+Qg1objNpXcHxP9KZwwp0XW4H/AHbzVhHEigZagb8xKDpQI1/8OSk2WZf9Bkr7CrsFC0hg== dependencies: - d3-path "^2.0.0" - d3-shape "^2.0.0" + d3-path "^3.0.1" + d3-shape "^3.1.0" vega-canvas "^1.2.5" - vega-loader "^4.3.3" - vega-scale "^7.1.1" + vega-loader "^4.4.0" + vega-scale "^7.2.0" vega-util "^1.15.2" vega-schema-url-parser@^2.2.0: @@ -29264,20 +29301,20 @@ vega-spec-injector@^0.0.2: resolved "https://registry.yarnpkg.com/vega-spec-injector/-/vega-spec-injector-0.0.2.tgz#f1d990109dd9d845c524738f818baa4b72a60ca6" integrity sha512-wOMMqmpssn0/ZFPW7wl1v26vbseRX7zHPWzEyS9TwNXTRCu1TcjIBIR+X23lCWocxhoBqFxmqyn8UowMhlGtAg== -vega-statistics@^1.7.9, vega-statistics@~1.7.10: - version "1.7.10" - resolved "https://registry.yarnpkg.com/vega-statistics/-/vega-statistics-1.7.10.tgz#4353637402e5e96bff2ebd16bd58e2c15cac3018" - integrity sha512-QLb12gcfpDZ9K5h3TLGrlz4UXDH9wSPyg9LLfOJZacxvvJEPohacUQNrGEAVtFO9ccUCerRfH9cs25ZtHsOZrw== +vega-statistics@^1.7.9, vega-statistics@^1.8.0, vega-statistics@~1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/vega-statistics/-/vega-statistics-1.8.0.tgz#ad66f7461473d58bc96671588981a059ffd60b59" + integrity sha512-dl+LCRS6qS4jWDme/NEdPVt5r649uB4IK6Kyr2/czmGA5JqjuFmtQ9lHQOnRu8945XLkqLf+JIQQo7vnw+nslA== dependencies: - d3-array "^2.7.1" + d3-array "^3.1.1" -vega-time@^2.0.3, vega-time@^2.0.4, vega-time@~2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/vega-time/-/vega-time-2.0.4.tgz#ff308358a831de927caa44e281cdc96f0863ba08" - integrity sha512-U314UDR9+ZlWrD3KBaeH+j/c2WSMdvcZq5yJfFT0yTg1jsBKAQBYFGvl+orackD8Zx3FveHOxx3XAObaQeDX+Q== +vega-time@^2.0.3, vega-time@^2.1.0, vega-time@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/vega-time/-/vega-time-2.1.0.tgz#acfbab88d7798b87ff63913b0dce2ca5eb0d46ca" + integrity sha512-Q9/l3S6Br1RPX5HZvyLD/cQ4K6K8DtpR09/1y7D66gxNorg2+HGzYZINH9nUvN3mxoXcBWg4cCUh3+JvmkDaEg== dependencies: - d3-array "^2.7.1" - d3-time "^2.0.0" + d3-array "^3.1.1" + d3-time "^3.0.0" vega-util "^1.15.2" vega-tooltip@^0.28.0: @@ -29287,15 +29324,15 @@ vega-tooltip@^0.28.0: dependencies: vega-util "^1.17.0" -vega-transforms@~4.9.4: - version "4.9.4" - resolved "https://registry.yarnpkg.com/vega-transforms/-/vega-transforms-4.9.4.tgz#5cf6b91bda9f184bbbaba63838be8e5e6a571235" - integrity sha512-JGBhm5Bf6fiGTUSB5Qr5ckw/KU9FJcSV5xIe/y4IobM/i/KNwI1i1fP45LzP4F4yZc0DMTwJod2UvFHGk9plKA== +vega-transforms@~4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/vega-transforms/-/vega-transforms-4.10.0.tgz#a1017ede13cf4e25499f588610a3be4da615d82d" + integrity sha512-Yk6ByzVq5F2niFfPlSsrU5wi+NZhsF7IBpJCcTfms4U7eoyNepUXagdFEJ3VWBD/Lit6GorLXFgO17NYcyS5gg== dependencies: - d3-array "^2.7.1" + d3-array "^3.1.1" vega-dataflow "^5.7.4" - vega-statistics "^1.7.9" - vega-time "^2.0.4" + vega-statistics "^1.8.0" + vega-time "^2.1.0" vega-util "^1.16.1" vega-typings@~0.22.0: @@ -29321,26 +29358,26 @@ vega-view-transforms@~4.5.8: vega-scenegraph "^4.9.2" vega-util "^1.15.2" -vega-view@~5.10.1: - version "5.10.1" - resolved "https://registry.yarnpkg.com/vega-view/-/vega-view-5.10.1.tgz#b69348bb32a9845a1bd341fdd946df98684fadc3" - integrity sha512-4xvQ5KZcgKdZx1Z7jjenCUumvlyr/j4XcHLRf9gyeFrFvvS596dVpL92V8twhV6O++DmS2+fj+rHagO8Di4nMg== +vega-view@~5.11.0: + version "5.11.0" + resolved "https://registry.yarnpkg.com/vega-view/-/vega-view-5.11.0.tgz#8a7b29a36776e43cc6599e087ed7f48a918b805d" + integrity sha512-MI9NTRFmtFX6ADk6KOHhi8bhHjC9pPm42Bj2+74c6l1d3NQZf9Jv7lkiGqKohdkQDNH9LPwz/6slhKwPU9JdkQ== dependencies: - d3-array "^2.7.1" - d3-timer "^2.0.0" + d3-array "^3.1.1" + d3-timer "^3.0.1" vega-dataflow "^5.7.3" - vega-format "^1.0.4" - vega-functions "^5.10.0" + vega-format "^1.1.0" + vega-functions "^5.13.0" vega-runtime "^6.1.3" - vega-scenegraph "^4.9.4" + vega-scenegraph "^4.10.0" vega-util "^1.16.1" -vega-voronoi@~4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/vega-voronoi/-/vega-voronoi-4.1.5.tgz#e7af574d4c27fd9cb12d70082f12c6f59b80b445" - integrity sha512-950IkgCFLj0zG33EWLAm1hZcp+FMqWcNQliMYt+MJzOD5S4MSpZpZ7K4wp2M1Jktjw/CLKFL9n38JCI0i3UonA== +vega-voronoi@~4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/vega-voronoi/-/vega-voronoi-4.2.0.tgz#14c74c84f52d9a16be2facd1bede879d32d988f2" + integrity sha512-1iuNAVZgUHRlBpdq4gSga3KlQmrgFfwy+KpyDgPLQ8HbLkhcVeT7RDh2L6naluqD7Op0xVLms3clR920WsYryQ== dependencies: - d3-delaunay "^5.3.0" + d3-delaunay "^6.0.2" vega-dataflow "^5.7.3" vega-util "^1.15.2" @@ -29355,37 +29392,37 @@ vega-wordcloud@~4.1.3: vega-statistics "^1.7.9" vega-util "^1.15.2" -vega@^5.21.0: - version "5.21.0" - resolved "https://registry.yarnpkg.com/vega/-/vega-5.21.0.tgz#f3d858d7544bfe4ffa3d8cd43d9ea978bf7391e8" - integrity sha512-yqqRa9nAqYoAxe7sVhRpsh0b001fly7Yx05klPkXmrvzjxXd07gClW1mOuGgSnVQqo7jTp/LYgbO1bD37FbEig== +vega@^5.22.0: + version "5.22.0" + resolved "https://registry.yarnpkg.com/vega/-/vega-5.22.0.tgz#9286832a0bc523ee39b1f3baff9a2ca1957a61f0" + integrity sha512-ZIehKTrMY93sWaWIn/2N2LwsCN8XymQthxQA5fQwTmefVl7OOvcYmsGFJ9nttXUF4n0z5WRXkSsPZcJHHBlOKw== dependencies: - vega-crossfilter "~4.0.5" + vega-crossfilter "~4.1.0" vega-dataflow "~5.7.4" - vega-encode "~4.8.3" + vega-encode "~4.9.0" vega-event-selector "~3.0.0" vega-expression "~5.0.0" - vega-force "~4.0.7" - vega-format "~1.0.4" - vega-functions "~5.12.1" - vega-geo "~4.3.8" - vega-hierarchy "~4.0.9" - vega-label "~1.1.0" - vega-loader "~4.4.1" + vega-force "~4.1.0" + vega-format "~1.1.0" + vega-functions "~5.13.0" + vega-geo "~4.4.0" + vega-hierarchy "~4.1.0" + vega-label "~1.2.0" + vega-loader "~4.5.0" vega-parser "~6.1.4" - vega-projection "~1.4.5" - vega-regression "~1.0.9" + vega-projection "~1.5.0" + vega-regression "~1.1.0" vega-runtime "~6.1.3" - vega-scale "~7.1.1" - vega-scenegraph "~4.9.4" - vega-statistics "~1.7.10" - vega-time "~2.0.4" - vega-transforms "~4.9.4" + vega-scale "~7.2.0" + vega-scenegraph "~4.10.0" + vega-statistics "~1.8.0" + vega-time "~2.1.0" + vega-transforms "~4.10.0" vega-typings "~0.22.0" vega-util "~1.17.0" - vega-view "~5.10.1" + vega-view "~5.11.0" vega-view-transforms "~4.5.8" - vega-voronoi "~4.1.5" + vega-voronoi "~4.2.0" vega-wordcloud "~4.1.3" vendors@^1.0.0: