diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index 6b8dc31bab34e..f2a58e7b6a7ac 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -44,7 +44,7 @@ kibanaPipeline(timeoutMinutes: 180) { 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), ]), ]) - workers.base(name: 'coverage-worker', label: 'tests-l', ramDisk: false, bootstrapped: false) { + workers.base(name: 'coverage-worker', size: 'l', ramDisk: false, bootstrapped: false) { kibanaPipeline.downloadCoverageArtifacts() kibanaPipeline.bash( ''' diff --git a/.ci/Jenkinsfile_visual_baseline b/.ci/Jenkinsfile_visual_baseline index 5c13ccccd9c6f..815c1345bbb68 100644 --- a/.ci/Jenkinsfile_visual_baseline +++ b/.ci/Jenkinsfile_visual_baseline @@ -7,12 +7,12 @@ kibanaPipeline(timeoutMinutes: 120) { catchError { parallel([ 'oss-visualRegression': { - workers.ci(name: 'oss-visualRegression', label: 'linux && immutable', ramDisk: false) { + workers.ci(name: 'oss-visualRegression', size: 's', ramDisk: false) { kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')(1) } }, 'xpack-visualRegression': { - workers.ci(name: 'xpack-visualRegression', label: 'linux && immutable', ramDisk: false) { + workers.ci(name: 'xpack-visualRegression', size: 's', ramDisk: false) { kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')(1) } }, diff --git a/.ci/es-snapshots/Jenkinsfile_build_es b/.ci/es-snapshots/Jenkinsfile_build_es index a00bcb3bbc946..a3470cd750738 100644 --- a/.ci/es-snapshots/Jenkinsfile_build_es +++ b/.ci/es-snapshots/Jenkinsfile_build_es @@ -25,7 +25,7 @@ def PROMOTE_WITHOUT_VERIFY = !!params.PROMOTE_WITHOUT_VERIFICATION timeout(time: 120, unit: 'MINUTES') { timestamps { ansiColor('xterm') { - node('linux && immutable') { + node(workers.label('s')) { catchErrors { def VERSION def SNAPSHOT_ID diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index ce472a404c053..ade79f27e10e9 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -61,7 +61,7 @@ kibanaPipeline(timeoutMinutes: 120) { } def promoteSnapshot(snapshotVersion, snapshotId) { - node('linux && immutable') { + node(workers.label('s')) { esSnapshots.promote(snapshotVersion, snapshotId) } } diff --git a/docs/apm/agent-configuration.asciidoc b/docs/apm/agent-configuration.asciidoc index 6f147d0e3223a..0d2834c1a400e 100644 --- a/docs/apm/agent-configuration.asciidoc +++ b/docs/apm/agent-configuration.asciidoc @@ -31,37 +31,18 @@ Kibana communicates any changed settings to APM Server so that your agents only [float] ==== Supported configurations -[float] -===== `CAPTURE_BODY` - -added[7.5.0] Can be `"off"`, `"errors"`, `"transactions"`, or `"all"`. Defaults to `"off"`. - -For transactions that are HTTP requests, the Agent can optionally capture the request body, e.g., POST variables. -Remember, request bodies often contain sensitive values like passwords, credit card numbers, etc. -If your service handles sensitive data, enable this feature with care. -Turning on body capturing can also significantly increase the overhead the overhead of the Agent, -and the Elasticsearch index size. - -[float] -===== `TRANSACTION_MAX_SPANS` - -added[7.5.0] A number between `0` and `32000`. Defaults to `500`. - -Limit the number of spans that are recorded per transaction. -This is helpful in cases where a transaction creates a very high amount of spans, e.g., thousands of SQL queries. -Setting an upper limit will help prevent the Agent and the APM Server from being overloaded. - -[float] -===== `TRANSACTION_SAMPLE_RATE` - -added[7.3.0] A sample rate between `0.000` and `1.0`. Default configuration is `1.0` (100% of traces). - -Adjusting the sampling rate controls what percent of requests are traced. -`1.0` means _all_ requests are traced. If you set the `TRANSACTION_SAMPLE_RATE` to a value below `1.0`, -the agent will randomly sample only a subset of transactions. -Unsampled transactions only record the name of the transaction, the overall transaction time, and the result. - -IMPORTANT: In a distributed trace, the sampling decision is propagated by the initializing Agent. -This means if you're using multiple agents, only the originating service's sampling rate will be used. -Be sure to set sensible defaults in _all_ of your agents, especially the -{apm-rum-ref}/configuration.html#transaction-sample-rate[JavaScript RUM Agent]. +Each Agent has its own list of supported configurations. +After selecting a Service name and environment in the APM app, +a list of all available configuration options, +including descriptions and default values, will be displayed. + +Supported configurations are also marked in each Agent's configuration documentation: + +[horizontal] +Go Agent:: {apm-go-ref}/configuration.html[Configuration reference] +Java Agent:: {apm-java-ref}/configuration.html[Configuration reference] +.NET Agent:: {apm-dotnet-ref}/configuration.html[Configuration reference] +Node.js Agent:: {apm-node-ref}/configuration.html[Configuration reference] +Python Agent:: {apm-py-ref}/configuration.html[Configuration reference] +Ruby Agent:: {apm-ruby-ref}/configuration.html[Configuration reference] +Real User Monitoring (RUM) Agent:: {apm-rum-ref}/configuration.html[Configuration reference] diff --git a/docs/apm/images/apm-agent-configuration.png b/docs/apm/images/apm-agent-configuration.png index d998b5daedd9b..ded0553219a03 100644 Binary files a/docs/apm/images/apm-agent-configuration.png and b/docs/apm/images/apm-agent-configuration.png differ diff --git a/docs/images/Dashboard_add_new_visualization.png b/docs/images/Dashboard_add_new_visualization.png index b871131805ab5..445f1f5dd8df5 100644 Binary files a/docs/images/Dashboard_add_new_visualization.png and b/docs/images/Dashboard_add_new_visualization.png differ diff --git a/docs/images/Dashboard_add_visualization.png b/docs/images/Dashboard_add_visualization.png index bc705b66e17d1..179dbc66eb194 100644 Binary files a/docs/images/Dashboard_add_visualization.png and b/docs/images/Dashboard_add_visualization.png differ diff --git a/docs/infrastructure/index.asciidoc b/docs/infrastructure/index.asciidoc index 60695c0e3f1cf..416e95a8941ce 100644 --- a/docs/infrastructure/index.asciidoc +++ b/docs/infrastructure/index.asciidoc @@ -21,6 +21,8 @@ You can optionally save these views and add them to {kibana-ref}/dashboard.html[ * Seamlessly switch to view the corresponding logs, application traces or uptime information for a component. +* Create alerts based on metric thresholds for one or more components. + To get started, you need to <>. Then you can <>. [role="screenshot"] diff --git a/docs/infrastructure/metrics-explorer.asciidoc b/docs/infrastructure/metrics-explorer.asciidoc index d47581ffe720a..793f09ea83b4f 100644 --- a/docs/infrastructure/metrics-explorer.asciidoc +++ b/docs/infrastructure/metrics-explorer.asciidoc @@ -20,6 +20,7 @@ By default that is set to `@timestamp`. * The interval for the X Axis is set to `auto`. The bucket size is determined by the time range. * To use *Open in Visualize* you need access to the Visualize app. +* To use *Create alert* you need to {kibana-ref}/alerting-getting-started.html#alerting-setup-prerequisites[set up alerting]. [float] [[metrics-explorer-tutorial]] @@ -67,4 +68,8 @@ Choose a graph, click the *Actions* dropdown and select *Open In Visualize*. This opens the graph in {kibana-ref}/TSVB.html[TSVB]. From here you can save the graph and add it to a dashboard as usual. +9. You can also create an alert based on the metrics in a graph. +Choose a graph, click the *Actions* dropdown and select *Create alert*. +This opens the {kibana-ref}/defining-alerts.html[alert flyout] prefilled with mertrics from the chart. + Who's the Metrics Explorer now? You are! diff --git a/docs/epm/index.asciidoc b/docs/ingest_manager/index.asciidoc similarity index 71% rename from docs/epm/index.asciidoc rename to docs/ingest_manager/index.asciidoc index d2ebe003afd6b..1254f412e14c5 100644 --- a/docs/epm/index.asciidoc +++ b/docs/ingest_manager/index.asciidoc @@ -1,8 +1,8 @@ [role="xpack"] [[epm]] -== Elastic Package Manager +== Ingest Manager -These are the docs for the Elastic Package Manager (EPM). +These are the docs for the Ingest Manager. === Configuration @@ -39,16 +39,71 @@ curl -X DELETE localhost:5601/api/ingest_manager/epm/packages/iptables-1.0.4 This section is to define terms used across ingest management. +==== Data Source + +A data source is a definition on how to collect data from a service, for example `nginx`. A data source contains +definitions for one or multiple inputs and each input can contain one or multiple streams. + +With the example of the nginx Data Source, it contains to inputs: `logs` and `nginx/metrics`. Logs and metrics are collected +differently. The `logs` input contains two streams, `access` and `error`, the `nginx/metrics` input contains the stubstatus stream. + + +==== Data Stream + +Data Streams are a [new concept](https://github.com/elastic/elasticsearch/issues/53100) in Elasticsearch which simplify +ingesting data and the setup of Elasticsearch. + ==== Elastic Agent + A single, unified agent that users can deploy to hosts or containers. It controls which data is collected from the host or containers and where the data is sent. It will run Beats, Endpoint or other monitoring programs as needed. It can operate standalone or pull a configuration policy from Fleet. + +==== Elastic Package Registry + +The Elastic Package Registry (EPR) is a service which runs under [https://epr.elastic.co]. It serves the packages through its API. +More details about the registry can be found [here](https://github.com/elastic/package-registry). + +==== Fleet + +Fleet is the part of the Ingest Manager UI in Kibana that handles the part of enrolling Elastic Agents, +managing agents and sending configurations to the Elastic Agent. + +==== Indexing Strategy + +Ingest Management + Elastic Agent follow a strict new indexing strategy: `{type}-{dataset}-{namespace}`. An example +for this is `logs-nginx.access-default`. More details about it can be found in the Index Strategy below. All data of +the index strategy is sent to Data Streams. + +==== Input + +An input is the configuration unit in an Agent Config that defines the options on how to collect data from +an endpoint. This could be username / password which are need to authenticate with a service or a host url +as an example. + +An input is part of a Data Source and contains streams. + +==== Integration + +An integration is a package with the type integration. An integration package has at least 1 data source +and usually collects data from / about a service. + + ==== Namespace + A user-specified string that will be used to part of the index name in Elasticsearch. It helps users identify logs coming from a specific environment (like prod or test), an application, or other identifiers. + ==== Package -A package contains all the assets for the Elastic Stack. A more detailed definition of a package can be found under https://github.com/elastic/package-registry. +A package contains all the assets for the Elastic Stack. A more detailed definition of a +package can be found under https://github.com/elastic/package-registry. + +Besides the assets, a package contains the data source definitions with its inputs and streams. + +==== Stream +A stream is a configuration unit in the Elastic Agent config. A stream is part of an input and defines how the data +fetched by this input should be processed and which Data Stream to send it to. == Indexing Strategy diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index 4dc03222f057e..f3e30eb7e7d77 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -141,4 +141,13 @@ access level. been deprecated with warnings that have been logged throughout 7.x. Please use Kibana UI to re-generate the POST URL snippets if you depend on these for automated PDF reports. +[float] +=== Configurations starting with `xpack.telemetry` are no longer valid + +*Details:* +The `xpack.` prefix has been removed for all telemetry configurations. + +*Impact:* +For any configurations beginning with `xpack.telemetry`, remove the `xpack` prefix. Use {kibana-ref}/telemetry-settings-kbn.html#telemetry-general-settings[`telemetry.enabled`] instead. + // end::notable-breaking-changes[] diff --git a/docs/settings/telemetry-settings.asciidoc b/docs/settings/telemetry-settings.asciidoc new file mode 100644 index 0000000000000..ad5f53ad879f8 --- /dev/null +++ b/docs/settings/telemetry-settings.asciidoc @@ -0,0 +1,38 @@ +[[telemetry-settings-kbn]] +=== Telemetry settings in Kibana +++++ +Telemetry settings +++++ + +By default, Usage Collection (also known as Telemetry) is enabled. This +helps us learn about the {kib} features that our users are most interested in, so we +can focus our efforts on making them even better. + +You can control whether this data is sent from the {kib} servers, or if it should be sent +from the user's browser, in case a firewall is blocking the connections from the server. Additionally, you can decide to completely disable this feature either in the config file or in {kib} via *Management > Kibana > Advanced Settings > Usage Data*. + +See our https://www.elastic.co/legal/privacy-statement[Privacy Statement] to learn more. + +[float] +[[telemetry-general-settings]] +==== General telemetry settings + +`telemetry.enabled`:: *Default: true*. +Set to `true` to send cluster statistics to Elastic. Reporting your +cluster statistics helps us improve your user experience. Your data is never +shared with anyone. Set to `false` to disable statistics reporting from any +browser connected to the {kib} instance. + +`telemetry.sendUsageFrom`:: *Default: 'browser'*. +Set to `'server'` to report the cluster statistics from the {kib} server. +If the server fails to connect to our endpoint at https://telemetry.elastic.co/, it assumes +it is behind a firewall and falls back to `'browser'` to send it from users' browsers +when they are navigating through {kib}. + +`telemetry.optIn`:: *Default: true*. +Set to `true` to automatically opt into reporting cluster statistics. You can also opt out through +*Advanced Settings* in {kib}. + +`telemetry.allowChangingOptInStatus`:: *Default: true*. +Set to `true` to allow overwriting the `telemetry.optIn` setting via the {kib} UI. +Note: When `false`, `telemetry.optIn` must be `true`. To disable telemetry and not allow users to change that parameter, use `telemetry.enabled`. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index fb58456ecc2cd..41fe8d337c03b 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -463,3 +463,4 @@ include::{docdir}/settings/reporting-settings.asciidoc[] include::secure-settings.asciidoc[] include::{docdir}/settings/security-settings.asciidoc[] include::{docdir}/settings/spaces-settings.asciidoc[] +include::{docdir}/settings/telemetry-settings.asciidoc[] diff --git a/docs/user/alerting/images/alert-concepts-summary.svg b/docs/user/alerting/images/alert-concepts-summary.svg index d11023b706418..0d63601c0693d 100644 --- a/docs/user/alerting/images/alert-concepts-summary.svg +++ b/docs/user/alerting/images/alert-concepts-summary.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/user/alerting/images/alert-instances.svg b/docs/user/alerting/images/alert-instances.svg index b7b0bd4996053..97f610041f0eb 100644 --- a/docs/user/alerting/images/alert-instances.svg +++ b/docs/user/alerting/images/alert-instances.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts b/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts index 2995c99ac9e58..b81a229ea23e7 100644 --- a/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts +++ b/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts @@ -21,11 +21,11 @@ import { i18n } from '@kbn/i18n'; import { IContainer, EmbeddableInput, - EmbeddableFactory, + EmbeddableFactoryDefinition, } from '../../../../src/plugins/embeddable/public'; import { HelloWorldEmbeddable, HELLO_WORLD_EMBEDDABLE } from './hello_world_embeddable'; -export class HelloWorldEmbeddableFactory extends EmbeddableFactory { +export class HelloWorldEmbeddableFactory implements EmbeddableFactoryDefinition { public readonly type = HELLO_WORLD_EMBEDDABLE; /** diff --git a/examples/embeddable_examples/public/list_container/list_container_factory.ts b/examples/embeddable_examples/public/list_container/list_container_factory.ts index 247cf48b41bde..1fde254110c62 100644 --- a/examples/embeddable_examples/public/list_container/list_container_factory.ts +++ b/examples/embeddable_examples/public/list_container/list_container_factory.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { - EmbeddableFactory, + EmbeddableFactoryDefinition, ContainerInput, EmbeddableStart, } from '../../../../src/plugins/embeddable/public'; @@ -29,22 +29,20 @@ interface StartServices { getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; } -export class ListContainerFactory extends EmbeddableFactory { +export class ListContainerFactory implements EmbeddableFactoryDefinition { public readonly type = LIST_CONTAINER; public readonly isContainerType = true; - constructor(private getStartServices: () => Promise) { - super(); - } + constructor(private getStartServices: () => Promise) {} public async isEditable() { return true; } - public async create(initialInput: ContainerInput) { + public create = async (initialInput: ContainerInput) => { const { getEmbeddableFactory } = await this.getStartServices(); return new ListContainer(initialInput, getEmbeddableFactory); - } + }; public getDisplayName() { return i18n.translate('embeddableExamples.searchableListContainer.displayName', { diff --git a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts index 9afdeabaee765..d010e25885ce6 100644 --- a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts +++ b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { IContainer, EmbeddableFactory } from '../../../../src/plugins/embeddable/public'; +import { IContainer, EmbeddableFactoryDefinition } from '../../../../src/plugins/embeddable/public'; import { MultiTaskTodoEmbeddable, MULTI_TASK_TODO_EMBEDDABLE, @@ -26,10 +26,8 @@ import { MultiTaskTodoOutput, } from './multi_task_todo_embeddable'; -export class MultiTaskTodoEmbeddableFactory extends EmbeddableFactory< - MultiTaskTodoInput, - MultiTaskTodoOutput -> { +export class MultiTaskTodoEmbeddableFactory + implements EmbeddableFactoryDefinition { public readonly type = MULTI_TASK_TODO_EMBEDDABLE; public async isEditable() { diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts index 79859cc015a80..382bb65e769ef 100644 --- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts +++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts @@ -18,7 +18,10 @@ */ import { i18n } from '@kbn/i18n'; -import { EmbeddableFactory, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import { + EmbeddableFactoryDefinition, + EmbeddableStart, +} from '../../../../src/plugins/embeddable/public'; import { SEARCHABLE_LIST_CONTAINER, SearchableListContainer, @@ -29,22 +32,20 @@ interface StartServices { getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; } -export class SearchableListContainerFactory extends EmbeddableFactory { +export class SearchableListContainerFactory implements EmbeddableFactoryDefinition { public readonly type = SEARCHABLE_LIST_CONTAINER; public readonly isContainerType = true; - constructor(private getStartServices: () => Promise) { - super(); - } + constructor(private getStartServices: () => Promise) {} public async isEditable() { return true; } - public async create(initialInput: SearchableContainerInput) { + public create = async (initialInput: SearchableContainerInput) => { const { getEmbeddableFactory } = await this.getStartServices(); return new SearchableListContainer(initialInput, getEmbeddableFactory); - } + }; public getDisplayName() { return i18n.translate('embeddableExamples.searchableListContainer.displayName', { diff --git a/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx b/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx index d7be436905382..bc577ca36d793 100644 --- a/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx +++ b/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx @@ -23,7 +23,7 @@ import { OverlayStart } from 'kibana/public'; import { EuiFieldText } from '@elastic/eui'; import { EuiButton } from '@elastic/eui'; import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; -import { IContainer, EmbeddableFactory } from '../../../../src/plugins/embeddable/public'; +import { IContainer, EmbeddableFactoryDefinition } from '../../../../src/plugins/embeddable/public'; import { TodoEmbeddable, TODO_EMBEDDABLE, TodoInput, TodoOutput } from './todo_embeddable'; function TaskInput({ onSave }: { onSave: (task: string) => void }) { @@ -47,16 +47,11 @@ interface StartServices { openModal: OverlayStart['openModal']; } -export class TodoEmbeddableFactory extends EmbeddableFactory< - TodoInput, - TodoOutput, - TodoEmbeddable -> { +export class TodoEmbeddableFactory + implements EmbeddableFactoryDefinition { public readonly type = TODO_EMBEDDABLE; - constructor(private getStartServices: () => Promise) { - super(); - } + constructor(private getStartServices: () => Promise) {} public async isEditable() { return true; @@ -72,7 +67,7 @@ export class TodoEmbeddableFactory extends EmbeddableFactory< * used to collect specific embeddable input that the container will not provide, like * in this case, the task string. */ - public async getExplicitInput() { + public getExplicitInput = async () => { const { openModal } = await this.getStartServices(); return new Promise<{ task: string }>(resolve => { const onSave = (task: string) => resolve({ task }); @@ -87,7 +82,7 @@ export class TodoEmbeddableFactory extends EmbeddableFactory< ) ); }); - } + }; public getDisplayName() { return i18n.translate('embeddableExamples.todo.displayName', { diff --git a/examples/embeddable_explorer/public/todo_embeddable_example.tsx b/examples/embeddable_explorer/public/todo_embeddable_example.tsx index ce92301236c2b..2af6c713593c6 100644 --- a/examples/embeddable_explorer/public/todo_embeddable_example.tsx +++ b/examples/embeddable_explorer/public/todo_embeddable_example.tsx @@ -37,9 +37,14 @@ import { import { TodoEmbeddable, TODO_EMBEDDABLE, - TodoEmbeddableFactory, + TodoInput, } from '../../../examples/embeddable_examples/public/todo'; -import { EmbeddableStart, EmbeddableRoot } from '../../../src/plugins/embeddable/public'; +import { + EmbeddableStart, + EmbeddableRoot, + EmbeddableOutput, + ErrorEmbeddable, +} from '../../../src/plugins/embeddable/public'; interface Props { getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; @@ -53,7 +58,7 @@ interface State { } export class TodoEmbeddableExample extends React.Component { - private embeddable?: TodoEmbeddable; + private embeddable?: TodoEmbeddable | ErrorEmbeddable; constructor(props: Props) { super(props); @@ -62,7 +67,9 @@ export class TodoEmbeddableExample extends React.Component { } public componentDidMount() { - const factory = this.props.getEmbeddableFactory(TODO_EMBEDDABLE) as TodoEmbeddableFactory; + const factory = this.props.getEmbeddableFactory( + TODO_EMBEDDABLE + ); if (factory === undefined) { throw new Error('Embeddable factory is undefined!'); diff --git a/package.json b/package.json index cdc41a2d8a3f0..46e0b9adfea25 100644 --- a/package.json +++ b/package.json @@ -238,6 +238,7 @@ "react-monaco-editor": "~0.27.0", "react-redux": "^7.1.3", "react-resize-detector": "^4.2.0", + "react-router": "^5.1.2", "react-router-dom": "^5.1.2", "react-sizeme": "^2.3.6", "react-use": "^13.27.0", @@ -395,7 +396,7 @@ "babel-eslint": "^10.0.3", "babel-jest": "^24.9.0", "babel-plugin-istanbul": "^5.2.0", - "backport": "5.1.2", + "backport": "5.1.3", "chai": "3.5.0", "chance": "1.0.18", "cheerio": "0.22.0", diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 9337daf419bfa..a3a11783cd82a 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -27,7 +27,7 @@ import TerserPlugin from 'terser-webpack-plugin'; import webpackMerge from 'webpack-merge'; // @ts-ignore import { CleanWebpackPlugin } from 'clean-webpack-plugin'; -import * as SharedDeps from '@kbn/ui-shared-deps'; +import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { Bundle, WorkerConfig, parseDirPath, DisallowedSyntaxPlugin } from '../common'; @@ -73,7 +73,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { }, externals: { - ...SharedDeps.externals, + ...UiSharedDeps.externals, }, plugins: [new CleanWebpackPlugin(), new DisallowedSyntaxPlugin()], diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index bcebdf591d6f0..5ea031595d1d4 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -38,7 +38,7 @@ "brace": "0.11.1", "chalk": "^2.4.2", "chokidar": "3.2.1", - "core-js": "^3.2.1", + "core-js": "^3.6.4", "css-loader": "^3.4.2", "expose-loader": "^0.7.5", "file-loader": "^4.2.0", diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index 5028c6efdb40e..f19271de8ad27 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -17,31 +17,40 @@ * under the License. */ -// import global polyfills before everything else require('./polyfills'); // must load before angular export const Jquery = require('jquery'); window.$ = window.jQuery = Jquery; -export const Angular = require('angular'); -export const ElasticCharts = require('@elastic/charts'); -export const ElasticEui = require('@elastic/eui'); -export const ElasticEuiLibServices = require('@elastic/eui/lib/services'); -export const ElasticEuiLightTheme = require('@elastic/eui/dist/eui_theme_light.json'); -export const ElasticEuiDarkTheme = require('@elastic/eui/dist/eui_theme_dark.json'); +// stateful deps export const KbnI18n = require('@kbn/i18n'); export const KbnI18nAngular = require('@kbn/i18n/angular'); export const KbnI18nReact = require('@kbn/i18n/react'); +export const Angular = require('angular'); export const Moment = require('moment'); export const MomentTimezone = require('moment-timezone/moment-timezone'); +export const Monaco = require('./monaco.ts'); +export const MonacoBare = require('monaco-editor/esm/vs/editor/editor.api'); export const React = require('react'); export const ReactDom = require('react-dom'); +export const ReactDomServer = require('react-dom/server'); export const ReactIntl = require('react-intl'); export const ReactRouter = require('react-router'); // eslint-disable-line export const ReactRouterDom = require('react-router-dom'); -export const Monaco = require('./monaco.ts'); -export const MonacoBare = require('monaco-editor/esm/vs/editor/editor.api'); -// load timezone data into moment-timezone Moment.tz.load(require('moment-timezone/data/packed/latest.json')); + +// big deps which are locked to a single version +export const Rxjs = require('rxjs'); +export const RxjsOperators = require('rxjs/operators'); +export const ElasticCharts = require('@elastic/charts'); +export const ElasticEui = require('@elastic/eui'); +export const ElasticEuiLibServices = require('@elastic/eui/lib/services'); +export const ElasticEuiLibServicesFormat = require('@elastic/eui/lib/services/format'); +export const ElasticEuiLightTheme = require('@elastic/eui/dist/eui_theme_light.json'); +export const ElasticEuiDarkTheme = require('@elastic/eui/dist/eui_theme_dark.json'); +export const ElasticEuiChartsTheme = require('@elastic/eui/dist/eui_charts_theme'); + +// massive deps that we should really get rid of or reduce in size substantially +export const ElasticsearchBrowser = require('elasticsearch-browser/elasticsearch.js'); diff --git a/packages/kbn-ui-shared-deps/index.d.ts b/packages/kbn-ui-shared-deps/index.d.ts index 7ee96050a1248..dec519da69641 100644 --- a/packages/kbn-ui-shared-deps/index.d.ts +++ b/packages/kbn-ui-shared-deps/index.d.ts @@ -25,7 +25,12 @@ export const distDir: string; /** * Filename of the main bundle file in the distributable directory */ -export const distFilename: string; +export const jsFilename: string; + +/** + * Filename of files that must be loaded before the jsFilename + */ +export const jsDepFilenames: string[]; /** * Filename of the unthemed css file in the distributable directory diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index d1bb93ddecd0a..666ec7a46ff06 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -20,17 +20,14 @@ const Path = require('path'); exports.distDir = Path.resolve(__dirname, 'target'); -exports.distFilename = 'kbn-ui-shared-deps.js'; +exports.jsDepFilenames = ['kbn-ui-shared-deps.@elastic.js']; +exports.jsFilename = 'kbn-ui-shared-deps.js'; exports.baseCssDistFilename = 'kbn-ui-shared-deps.css'; exports.lightCssDistFilename = 'kbn-ui-shared-deps.light.css'; exports.darkCssDistFilename = 'kbn-ui-shared-deps.dark.css'; exports.externals = { + // stateful deps angular: '__kbnSharedDeps__.Angular', - '@elastic/charts': '__kbnSharedDeps__.ElasticCharts', - '@elastic/eui': '__kbnSharedDeps__.ElasticEui', - '@elastic/eui/lib/services': '__kbnSharedDeps__.ElasticEuiLibServices', - '@elastic/eui/dist/eui_theme_light.json': '__kbnSharedDeps__.ElasticEuiLightTheme', - '@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.ElasticEuiDarkTheme', '@kbn/i18n': '__kbnSharedDeps__.KbnI18n', '@kbn/i18n/angular': '__kbnSharedDeps__.KbnI18nAngular', '@kbn/i18n/react': '__kbnSharedDeps__.KbnI18nReact', @@ -39,10 +36,31 @@ exports.externals = { 'moment-timezone': '__kbnSharedDeps__.MomentTimezone', react: '__kbnSharedDeps__.React', 'react-dom': '__kbnSharedDeps__.ReactDom', + 'react-dom/server': '__kbnSharedDeps__.ReactDomServer', 'react-intl': '__kbnSharedDeps__.ReactIntl', 'react-router': '__kbnSharedDeps__.ReactRouter', 'react-router-dom': '__kbnSharedDeps__.ReactRouterDom', '@kbn/ui-shared-deps/monaco': '__kbnSharedDeps__.Monaco', // this is how plugins/consumers from npm load monaco 'monaco-editor/esm/vs/editor/editor.api': '__kbnSharedDeps__.MonacoBare', + + /** + * big deps which are locked to a single version + */ + rxjs: '__kbnSharedDeps__.Rxjs', + 'rxjs/operators': '__kbnSharedDeps__.RxjsOperators', + '@elastic/charts': '__kbnSharedDeps__.ElasticCharts', + '@elastic/eui': '__kbnSharedDeps__.ElasticEui', + '@elastic/eui/lib/services': '__kbnSharedDeps__.ElasticEuiLibServices', + '@elastic/eui/lib/services/format': '__kbnSharedDeps__.ElasticEuiLibServicesFormat', + '@elastic/eui/dist/eui_charts_theme': '__kbnSharedDeps__.ElasticEuiChartsTheme', + '@elastic/eui/dist/eui_theme_light.json': '__kbnSharedDeps__.ElasticEuiLightTheme', + '@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.ElasticEuiDarkTheme', + + /** + * massive deps that we should really get rid of or reduce in size substantially + */ + elasticsearch: '__kbnSharedDeps__.ElasticsearchBrowser', + 'elasticsearch-browser': '__kbnSharedDeps__.ElasticsearchBrowser', + 'elasticsearch-browser/elasticsearch': '__kbnSharedDeps__.ElasticsearchBrowser', }; diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index c76e909d2adbc..e2823f23d0431 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -1,37 +1,41 @@ { "name": "@kbn/ui-shared-deps", "version": "1.0.0", - "license": "Apache-2.0", "private": true, + "license": "Apache-2.0", "scripts": { "build": "node scripts/build", "kbn:bootstrap": "node scripts/build --dev", "kbn:watch": "node scripts/build --watch" }, - "devDependencies": { + "dependencies": { "@elastic/charts": "^18.1.1", - "abortcontroller-polyfill": "^1.4.0", "@elastic/eui": "21.0.1", - "@kbn/babel-preset": "1.0.0", - "@kbn/dev-utils": "1.0.0", "@kbn/i18n": "1.0.0", - "@yarnpkg/lockfile": "^1.1.0", + "abortcontroller-polyfill": "^1.4.0", "angular": "^1.7.9", - "core-js": "^3.2.1", - "css-loader": "^3.4.2", + "core-js": "^3.6.4", "custom-event-polyfill": "^0.3.0", - "del": "^5.1.0", + "elasticsearch-browser": "^16.7.0", "jquery": "^3.4.1", - "mini-css-extract-plugin": "0.8.0", "moment": "^2.24.0", "moment-timezone": "^0.5.27", + "monaco-editor": "~0.17.0", "react": "^16.12.0", "react-dom": "^16.12.0", "react-intl": "^2.8.0", - "read-pkg": "^5.2.0", + "react-router": "^5.1.2", + "react-router-dom": "^5.1.2", "regenerator-runtime": "^0.13.3", + "rxjs": "^6.5.3", "symbol-observable": "^1.2.0", - "webpack": "^4.41.5", "whatwg-fetch": "^3.0.0" + }, + "devDependencies": { + "@kbn/babel-preset": "1.0.0", + "@kbn/dev-utils": "1.0.0", + "css-loader": "^3.4.2", + "del": "^5.1.0", + "webpack": "^4.41.5" } } diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index dc6e7ae33dbec..a875274544905 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -23,19 +23,19 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const { REPO_ROOT } = require('@kbn/dev-utils'); const webpack = require('webpack'); -const SharedDeps = require('./index'); +const UiSharedDeps = require('./index'); const MOMENT_SRC = require.resolve('moment/min/moment-with-locales.js'); exports.getWebpackConfig = ({ dev = false } = {}) => ({ mode: dev ? 'development' : 'production', entry: { - [SharedDeps.distFilename.replace(/\.js$/, '')]: './entry.js', - [SharedDeps.darkCssDistFilename.replace(/\.css$/, '')]: [ + 'kbn-ui-shared-deps': './entry.js', + 'kbn-ui-shared-deps.dark': [ '@elastic/eui/dist/eui_theme_dark.css', '@elastic/charts/dist/theme_only_dark.css', ], - [SharedDeps.lightCssDistFilename.replace(/\.css$/, '')]: [ + 'kbn-ui-shared-deps.light': [ '@elastic/eui/dist/eui_theme_light.css', '@elastic/charts/dist/theme_only_light.css', ], @@ -43,7 +43,7 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ context: __dirname, devtool: dev ? '#cheap-source-map' : false, output: { - path: SharedDeps.distDir, + path: UiSharedDeps.distDir, filename: '[name].js', sourceMapFilename: '[file].map', publicPath: '__REPLACE_WITH_PUBLIC_PATH__', @@ -81,6 +81,16 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ optimization: { noEmitOnErrors: true, + splitChunks: { + cacheGroups: { + 'kbn-ui-shared-deps.@elastic': { + name: 'kbn-ui-shared-deps.@elastic', + test: m => m.resource && m.resource.includes('@elastic'), + chunks: 'all', + enforce: true, + }, + }, + }, }, performance: { diff --git a/packages/kbn-ui-shared-deps/yarn.lock b/packages/kbn-ui-shared-deps/yarn.lock new file mode 120000 index 0000000000000..3f82ebc9cdbae --- /dev/null +++ b/packages/kbn-ui-shared-deps/yarn.lock @@ -0,0 +1 @@ +../../yarn.lock \ No newline at end of file diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index d91e55115d0b1..f0b21adf62ff7 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -128,13 +128,6 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ renameFromRoot('optimize.lazyHost', 'optimize.watchHost'), renameFromRoot('optimize.lazyPrebuild', 'optimize.watchPrebuild'), renameFromRoot('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'), - renameFromRoot('xpack.xpack_main.telemetry.config', 'telemetry.config'), - renameFromRoot('xpack.xpack_main.telemetry.url', 'telemetry.url'), - renameFromRoot('xpack.xpack_main.telemetry.enabled', 'telemetry.enabled'), - renameFromRoot('xpack.telemetry.enabled', 'telemetry.enabled'), - renameFromRoot('xpack.telemetry.config', 'telemetry.config'), - renameFromRoot('xpack.telemetry.banner', 'telemetry.banner'), - renameFromRoot('xpack.telemetry.url', 'telemetry.url'), // Monitoring renames // TODO: Remove these from here once the monitoring plugin is migrated to NP renameFromRoot('xpack.monitoring.enabled', 'monitoring.enabled'), diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts b/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts index d908fdbfd2e80..df8566746eaf9 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts @@ -22,7 +22,7 @@ jest.mock('fs', () => ({ readFileSync: mockReadFileSync })); export const mockReadPkcs12Keystore = jest.fn(); export const mockReadPkcs12Truststore = jest.fn(); -jest.mock('../../utils', () => ({ +jest.mock('../utils', () => ({ readPkcs12Keystore: mockReadPkcs12Keystore, readPkcs12Truststore: mockReadPkcs12Truststore, })); diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index 1b4fc5eafec76..de3f57298f461 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -227,7 +227,7 @@ describe('throws when config is invalid', () => { beforeAll(() => { const realFs = jest.requireActual('fs'); mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path)); - const utils = jest.requireActual('../../utils'); + const utils = jest.requireActual('../utils'); mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => utils.readPkcs12Keystore(path, password) ); diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index b2f4e388b337d..d3012e361b3ed 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -21,7 +21,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { Duration } from 'moment'; import { readFileSync } from 'fs'; import { ConfigDeprecationProvider } from 'src/core/server'; -import { readPkcs12Keystore, readPkcs12Truststore } from '../../utils'; +import { readPkcs12Keystore, readPkcs12Truststore } from '../utils'; import { ServiceConfigDescriptor } from '../internal_types'; const hostURISchema = schema.uri({ scheme: ['http', 'https'] }); diff --git a/src/core/server/http/ssl_config.test.mocks.ts b/src/core/server/http/ssl_config.test.mocks.ts index ab98c3a27920c..50e34c7c8f678 100644 --- a/src/core/server/http/ssl_config.test.mocks.ts +++ b/src/core/server/http/ssl_config.test.mocks.ts @@ -24,7 +24,7 @@ jest.mock('fs', () => { export const mockReadPkcs12Keystore = jest.fn(); export const mockReadPkcs12Truststore = jest.fn(); -jest.mock('../../utils', () => ({ +jest.mock('../utils', () => ({ readPkcs12Keystore: mockReadPkcs12Keystore, readPkcs12Truststore: mockReadPkcs12Truststore, })); diff --git a/src/core/server/http/ssl_config.test.ts b/src/core/server/http/ssl_config.test.ts index 3980b9c247fa3..5d0bed601f540 100644 --- a/src/core/server/http/ssl_config.test.ts +++ b/src/core/server/http/ssl_config.test.ts @@ -45,7 +45,7 @@ describe('#SslConfig', () => { beforeEach(() => { const realFs = jest.requireActual('fs'); mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path)); - const utils = jest.requireActual('../../utils'); + const utils = jest.requireActual('../utils'); mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => utils.readPkcs12Keystore(path, password) ); diff --git a/src/core/server/http/ssl_config.ts b/src/core/server/http/ssl_config.ts index 0096eeb092565..4eb0c50e72362 100644 --- a/src/core/server/http/ssl_config.ts +++ b/src/core/server/http/ssl_config.ts @@ -20,7 +20,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import crypto from 'crypto'; import { readFileSync } from 'fs'; -import { readPkcs12Keystore, readPkcs12Truststore } from '../../utils'; +import { readPkcs12Keystore, readPkcs12Truststore } from '../utils'; // `crypto` type definitions doesn't currently include `crypto.constants`, see // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/fa5baf1733f49cf26228a4e509914572c1b74adf/types/node/v6/index.d.ts#L3412 diff --git a/src/core/utils/crypto/__fixtures__/README.md b/src/core/server/utils/crypto/__fixtures__/README.md similarity index 100% rename from src/core/utils/crypto/__fixtures__/README.md rename to src/core/server/utils/crypto/__fixtures__/README.md diff --git a/src/core/utils/crypto/__fixtures__/index.ts b/src/core/server/utils/crypto/__fixtures__/index.ts similarity index 100% rename from src/core/utils/crypto/__fixtures__/index.ts rename to src/core/server/utils/crypto/__fixtures__/index.ts diff --git a/src/core/utils/crypto/__fixtures__/no_ca.p12 b/src/core/server/utils/crypto/__fixtures__/no_ca.p12 similarity index 100% rename from src/core/utils/crypto/__fixtures__/no_ca.p12 rename to src/core/server/utils/crypto/__fixtures__/no_ca.p12 diff --git a/src/core/utils/crypto/__fixtures__/no_cert.p12 b/src/core/server/utils/crypto/__fixtures__/no_cert.p12 similarity index 100% rename from src/core/utils/crypto/__fixtures__/no_cert.p12 rename to src/core/server/utils/crypto/__fixtures__/no_cert.p12 diff --git a/src/core/utils/crypto/__fixtures__/no_key.p12 b/src/core/server/utils/crypto/__fixtures__/no_key.p12 similarity index 100% rename from src/core/utils/crypto/__fixtures__/no_key.p12 rename to src/core/server/utils/crypto/__fixtures__/no_key.p12 diff --git a/src/core/utils/crypto/__fixtures__/two_cas.p12 b/src/core/server/utils/crypto/__fixtures__/two_cas.p12 similarity index 100% rename from src/core/utils/crypto/__fixtures__/two_cas.p12 rename to src/core/server/utils/crypto/__fixtures__/two_cas.p12 diff --git a/src/core/utils/crypto/__fixtures__/two_keys.p12 b/src/core/server/utils/crypto/__fixtures__/two_keys.p12 similarity index 100% rename from src/core/utils/crypto/__fixtures__/two_keys.p12 rename to src/core/server/utils/crypto/__fixtures__/two_keys.p12 diff --git a/src/core/utils/crypto/index.ts b/src/core/server/utils/crypto/index.ts similarity index 100% rename from src/core/utils/crypto/index.ts rename to src/core/server/utils/crypto/index.ts diff --git a/src/core/utils/crypto/pkcs12.test.ts b/src/core/server/utils/crypto/pkcs12.test.ts similarity index 99% rename from src/core/utils/crypto/pkcs12.test.ts rename to src/core/server/utils/crypto/pkcs12.test.ts index c6c28697c4bcc..e9eb72fe395ff 100644 --- a/src/core/utils/crypto/pkcs12.test.ts +++ b/src/core/server/utils/crypto/pkcs12.test.ts @@ -29,7 +29,7 @@ import { import { NO_CA_PATH, NO_CERT_PATH, NO_KEY_PATH, TWO_CAS_PATH, TWO_KEYS_PATH } from './__fixtures__'; import { readFileSync } from 'fs'; -import { readPkcs12Keystore, Pkcs12ReadResult, readPkcs12Truststore } from '.'; +import { readPkcs12Keystore, Pkcs12ReadResult, readPkcs12Truststore } from './index'; const reformatPem = (pem: string) => { // ensure consistency in line endings when comparing two PEM files diff --git a/src/core/utils/crypto/pkcs12.ts b/src/core/server/utils/crypto/pkcs12.ts similarity index 100% rename from src/core/utils/crypto/pkcs12.ts rename to src/core/server/utils/crypto/pkcs12.ts diff --git a/src/core/server/utils/index.ts b/src/core/server/utils/index.ts index 86924c559e5fa..b01a4c4e04899 100644 --- a/src/core/server/utils/index.ts +++ b/src/core/server/utils/index.ts @@ -17,5 +17,6 @@ * under the License. */ +export * from './crypto'; export * from './from_root'; export * from './package_json'; diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index e35356343cfe2..a6df0992f6cc6 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -19,7 +19,6 @@ export * from './assert_never'; export * from './context'; -export * from './crypto'; export * from './deep_freeze'; export * from './get'; export * from './map_to_object'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index a39266ecd8db3..e38345989598d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -36,6 +36,7 @@ import { IndexPattern, IndexPatternsContract, Query, + QueryState, SavedQuery, syncQueryStateWithUrl, } from '../../../../../../plugins/data/public'; @@ -48,7 +49,6 @@ import { import { DASHBOARD_CONTAINER_TYPE, DashboardContainer, - DashboardContainerFactory, DashboardContainerInput, DashboardPanelState, } from '../../../../../../plugins/dashboard/public'; @@ -58,6 +58,7 @@ import { isErrorEmbeddable, openAddPanelFlyout, ViewMode, + ContainerOutput, } from '../../../../../../plugins/embeddable/public'; import { NavAction, SavedDashboardPanel } from './types'; @@ -132,13 +133,6 @@ export class DashboardAppController { const queryFilter = filterManager; const timefilter = queryService.timefilter.timefilter; - // starts syncing `_g` portion of url with query services - // note: dashboard_state_manager.ts syncs `_a` portion of url - const { - stop: stopSyncingQueryServiceStateWithUrl, - hasInheritedQueryFromUrl: hasInheritedGlobalStateFromUrl, - } = syncQueryStateWithUrl(queryService, kbnUrlStateStorage); - let lastReloadRequestTime = 0; const dash = ($scope.dash = $route.current.locals.dash); if (dash.id) { @@ -170,9 +164,24 @@ export class DashboardAppController { // The hash check is so we only update the time filter on dashboard open, not during // normal cross app navigation. - if (dashboardStateManager.getIsTimeSavedWithDashboard() && !hasInheritedGlobalStateFromUrl) { - dashboardStateManager.syncTimefilterWithDashboard(timefilter); + if (dashboardStateManager.getIsTimeSavedWithDashboard()) { + const initialGlobalStateInUrl = kbnUrlStateStorage.get('_g'); + if (!initialGlobalStateInUrl?.time) { + dashboardStateManager.syncTimefilterWithDashboardTime(timefilter); + } + if (!initialGlobalStateInUrl?.refreshInterval) { + dashboardStateManager.syncTimefilterWithDashboardRefreshInterval(timefilter); + } } + + // starts syncing `_g` portion of url with query services + // note: dashboard_state_manager.ts syncs `_a` portion of url + // it is important to start this syncing after `dashboardStateManager.syncTimefilterWithDashboard(timefilter);` above is run, + // otherwise it will case redundant browser history record + const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl( + queryService, + kbnUrlStateStorage + ); $scope.showSaveQuery = dashboardCapabilities.saveQuery as boolean; const getShouldShowEditHelp = () => @@ -307,83 +316,92 @@ export class DashboardAppController { let outputSubscription: Subscription | undefined; const dashboardDom = document.getElementById('dashboardViewport'); - const dashboardFactory = embeddable.getEmbeddableFactory( - DASHBOARD_CONTAINER_TYPE - ) as DashboardContainerFactory; - dashboardFactory - .create(getDashboardInput()) - .then((container: DashboardContainer | ErrorEmbeddable) => { - if (!isErrorEmbeddable(container)) { - dashboardContainer = container; - - dashboardContainer.renderEmpty = () => { - const shouldShowEditHelp = getShouldShowEditHelp(); - const shouldShowViewHelp = getShouldShowViewHelp(); - const isEmptyInReadOnlyMode = shouldShowUnauthorizedEmptyState(); - const isEmptyState = shouldShowEditHelp || shouldShowViewHelp || isEmptyInReadOnlyMode; - return isEmptyState ? ( - - ) : null; - }; - - updateIndexPatterns(dashboardContainer); - - outputSubscription = dashboardContainer.getOutput$().subscribe(() => { - updateIndexPatterns(dashboardContainer); - }); - - inputSubscription = dashboardContainer.getInput$().subscribe(() => { - let dirty = false; - - // This has to be first because handleDashboardContainerChanges causes - // appState.save which will cause refreshDashboardContainer to be called. + const dashboardFactory = embeddable.getEmbeddableFactory< + DashboardContainerInput, + ContainerOutput, + DashboardContainer + >(DASHBOARD_CONTAINER_TYPE); + + if (dashboardFactory) { + dashboardFactory + .create(getDashboardInput()) + .then((container: DashboardContainer | ErrorEmbeddable | undefined) => { + if (container && !isErrorEmbeddable(container)) { + dashboardContainer = container; + + dashboardContainer.renderEmpty = () => { + const shouldShowEditHelp = getShouldShowEditHelp(); + const shouldShowViewHelp = getShouldShowViewHelp(); + const isEmptyInReadOnlyMode = shouldShowUnauthorizedEmptyState(); + const isEmptyState = + shouldShowEditHelp || shouldShowViewHelp || isEmptyInReadOnlyMode; + return isEmptyState ? ( + + ) : null; + }; - if ( - !esFilters.compareFilters( - container.getInput().filters, - queryFilter.getFilters(), - esFilters.COMPARE_ALL_OPTIONS - ) - ) { - // Add filters modifies the object passed to it, hence the clone deep. - queryFilter.addFilters(_.cloneDeep(container.getInput().filters)); + updateIndexPatterns(dashboardContainer); - dashboardStateManager.applyFilters($scope.model.query, container.getInput().filters); - dirty = true; - } + outputSubscription = dashboardContainer.getOutput$().subscribe(() => { + updateIndexPatterns(dashboardContainer); + }); - dashboardStateManager.handleDashboardContainerChanges(container); - $scope.$evalAsync(() => { - if (dirty) { - updateState(); + inputSubscription = dashboardContainer.getInput$().subscribe(() => { + let dirty = false; + + // This has to be first because handleDashboardContainerChanges causes + // appState.save which will cause refreshDashboardContainer to be called. + + if ( + !esFilters.compareFilters( + container.getInput().filters, + queryFilter.getFilters(), + esFilters.COMPARE_ALL_OPTIONS + ) + ) { + // Add filters modifies the object passed to it, hence the clone deep. + queryFilter.addFilters(_.cloneDeep(container.getInput().filters)); + + dashboardStateManager.applyFilters( + $scope.model.query, + container.getInput().filters + ); + dirty = true; } + + dashboardStateManager.handleDashboardContainerChanges(container); + $scope.$evalAsync(() => { + if (dirty) { + updateState(); + } + }); }); - }); - dashboardStateManager.registerChangeListener(() => { - // we aren't checking dirty state because there are changes the container needs to know about - // that won't make the dashboard "dirty" - like a view mode change. - refreshDashboardContainer(); - }); + dashboardStateManager.registerChangeListener(() => { + // we aren't checking dirty state because there are changes the container needs to know about + // that won't make the dashboard "dirty" - like a view mode change. + refreshDashboardContainer(); + }); - // This code needs to be replaced with a better mechanism for adding new embeddables of - // any type from the add panel. Likely this will happen via creating a visualization "inline", - // without navigating away from the UX. - if ($routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]) { - const type = $routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]; - const id = $routeParams[DashboardConstants.ADD_EMBEDDABLE_ID]; - container.addSavedObjectEmbeddable(type, id); - removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_TYPE); - removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_ID); + // This code needs to be replaced with a better mechanism for adding new embeddables of + // any type from the add panel. Likely this will happen via creating a visualization "inline", + // without navigating away from the UX. + if ($routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]) { + const type = $routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]; + const id = $routeParams[DashboardConstants.ADD_EMBEDDABLE_ID]; + container.addSavedObjectEmbeddable(type, id); + removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_TYPE); + removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_ID); + } } - } - if (dashboardDom) { - container.render(dashboardDom); - } - }); + if (dashboardDom && container) { + container.render(dashboardDom); + } + }); + } // Part of the exposed plugin API - do not remove without careful consideration. this.appStatus = { @@ -643,6 +661,14 @@ export class DashboardAppController { // This is only necessary for new dashboards, which will default to Edit mode. updateViewMode(ViewMode.VIEW); + // We need to do a hard reset of the timepicker. appState will not reload like + // it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on + // reload will cause it not to sync. + if (dashboardStateManager.getIsTimeSavedWithDashboard()) { + dashboardStateManager.syncTimefilterWithDashboardTime(timefilter); + dashboardStateManager.syncTimefilterWithDashboardRefreshInterval(timefilter); + } + // Angular's $location skips this update because of history updates from syncState which happen simultaneously // when calling kbnUrl.change() angular schedules url update and when angular finally starts to process it, // the update is considered outdated and angular skips it @@ -650,19 +676,6 @@ export class DashboardAppController { dashboardStateManager.changeDashboardUrl( dash.id ? createDashboardEditUrl(dash.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL ); - - // We need to do a hard reset of the timepicker. appState will not reload like - // it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on - // reload will cause it not to sync. - if (dashboardStateManager.getIsTimeSavedWithDashboard()) { - // have to use $evalAsync here until '_g' is migrated from $location to state sync utility ('history') - // When state sync utility changes url, angular's $location is missing it's own updates which happen during the same digest cycle - // temporary solution is to delay $location updates to next digest cycle - // unfortunately, these causes 2 browser history entries, but this is temporary and will be fixed after migrating '_g' to state_sync utilities - $scope.$evalAsync(() => { - dashboardStateManager.syncTimefilterWithDashboard(timefilter); - }); - } } overlays diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts index 08ccc1e0d1e89..14af89f80f9aa 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts @@ -59,7 +59,7 @@ describe('DashboardState', function() { mockTime.to = '2015-09-29 06:31:44.000'; initDashboardState(); - dashboardState.syncTimefilterWithDashboard(mockTimefilter); + dashboardState.syncTimefilterWithDashboardTime(mockTimefilter); expect(mockTime.to).toBe('now/w'); expect(mockTime.from).toBe('now/w'); @@ -74,7 +74,7 @@ describe('DashboardState', function() { mockTime.to = '2015-09-29 06:31:44.000'; initDashboardState(); - dashboardState.syncTimefilterWithDashboard(mockTimefilter); + dashboardState.syncTimefilterWithDashboardTime(mockTimefilter); expect(mockTime.to).toBe('now'); expect(mockTime.from).toBe('now-13d'); @@ -89,7 +89,7 @@ describe('DashboardState', function() { mockTime.to = 'now/w'; initDashboardState(); - dashboardState.syncTimefilterWithDashboard(mockTimefilter); + dashboardState.syncTimefilterWithDashboardTime(mockTimefilter); expect(mockTime.to).toBe(savedDashboard.timeTo); expect(mockTime.from).toBe(savedDashboard.timeFrom); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts index 171f08b45cf8d..9b8f75bdcf953 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts @@ -35,7 +35,7 @@ import { TimefilterContract as Timefilter, } from '../../../../../../plugins/data/public'; -import { getAppStateDefaults, migrateAppState } from './lib'; +import { getAppStateDefaults, migrateAppState, getDashboardIdFromUrl } from './lib'; import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_object_converters'; import { FilterUtils } from './lib/filter_utils'; import { @@ -175,6 +175,14 @@ export class DashboardStateManager { // sync state required state container to be able to handle null // overriding set() so it could handle null coming from url if (state) { + // Skip this update if current dashboardId in the url is different from what we have in the current instance of state manager + // As dashboard is driven by angular at the moment, the destroy cycle happens async, + // If the dashboardId has changed it means this instance + // is going to be destroyed soon and we shouldn't sync state anymore, + // as it could potentially trigger further url updates + const currentDashboardIdInUrl = getDashboardIdFromUrl(history.location.pathname); + if (currentDashboardIdInUrl !== this.savedDashboard.id) return; + this.stateContainer.set({ ...this.stateDefaults, ...state, @@ -203,6 +211,7 @@ export class DashboardStateManager { public handleDashboardContainerChanges(dashboardContainer: DashboardContainer) { let dirty = false; + let dirtyBecauseOfInitialStateMigration = false; const savedDashboardPanelMap: { [key: string]: SavedDashboardPanel } = {}; @@ -236,11 +245,20 @@ export class DashboardStateManager { ) { // A panel was changed dirty = true; + + const oldVersion = savedDashboardPanelMap[panelState.explicitInput.id]?.version; + const newVersion = convertedPanelStateMap[panelState.explicitInput.id]?.version; + if (oldVersion && newVersion && oldVersion !== newVersion) { + dirtyBecauseOfInitialStateMigration = true; + } } }); if (dirty) { this.stateContainer.transitions.set('panels', Object.values(convertedPanelStateMap)); + if (dirtyBecauseOfInitialStateMigration) { + this.saveState({ replace: true }); + } } if (input.isFullScreenMode !== this.getFullScreenMode()) { @@ -498,7 +516,7 @@ export class DashboardStateManager { * @param timeFilter.setTime * @param timeFilter.setRefreshInterval */ - public syncTimefilterWithDashboard(timeFilter: Timefilter) { + public syncTimefilterWithDashboardTime(timeFilter: Timefilter) { if (!this.getIsTimeSavedWithDashboard()) { throw new Error( i18n.translate('kbn.dashboard.stateManager.timeNotSavedWithDashboardErrorMessage', { @@ -513,6 +531,20 @@ export class DashboardStateManager { to: this.savedDashboard.timeTo, }); } + } + + /** + * Updates timeFilter to match the refreshInterval saved with the dashboard. + * @param timeFilter + */ + public syncTimefilterWithDashboardRefreshInterval(timeFilter: Timefilter) { + if (!this.getIsTimeSavedWithDashboard()) { + throw new Error( + i18n.translate('kbn.dashboard.stateManager.timeNotSavedWithDashboardErrorMessage', { + defaultMessage: 'The time is not saved with this dashboard so should not be synced.', + }) + ); + } if (this.savedDashboard.refreshInterval) { timeFilter.setRefreshInterval(this.savedDashboard.refreshInterval); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/index.ts index b4c9e939d3083..e9ebe73c3b34d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/index.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/index.ts @@ -20,3 +20,4 @@ export { saveDashboard } from './save_dashboard'; export { getAppStateDefaults } from './get_app_state_defaults'; export { migrateAppState } from './migrate_app_state'; +export { getDashboardIdFromUrl } from './url'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/url.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/url.test.ts new file mode 100644 index 0000000000000..70a9d86206fd6 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/url.test.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getDashboardIdFromUrl } from './url'; + +test('getDashboardIdFromUrl', () => { + let url = + "http://localhost:5601/wev/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; + expect(getDashboardIdFromUrl(url)).toEqual(undefined); + + url = + "http://localhost:5601/wev/app/kibana#/dashboard/625357282?_a=(description:'',filters:!()&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))"; + expect(getDashboardIdFromUrl(url)).toEqual('625357282'); + + url = 'http://myserver.mydomain.com:5601/wev/app/kibana#/dashboard/777182'; + expect(getDashboardIdFromUrl(url)).toEqual('777182'); + + url = + "http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; + expect(getDashboardIdFromUrl(url)).toEqual(undefined); + + url = '/dashboard/test/?_g=(refreshInterval:'; + expect(getDashboardIdFromUrl(url)).toEqual('test'); + + url = 'dashboard/test/?_g=(refreshInterval:'; + expect(getDashboardIdFromUrl(url)).toEqual('test'); + + url = '/other-app/test/'; + expect(getDashboardIdFromUrl(url)).toEqual(undefined); +}); diff --git a/webpackShims/elasticsearch-browser.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/url.ts similarity index 55% rename from webpackShims/elasticsearch-browser.js rename to src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/url.ts index a4373dcdfe1d1..2489867fa6233 100644 --- a/webpackShims/elasticsearch-browser.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/url.ts @@ -17,5 +17,19 @@ * under the License. */ -require('angular'); -module.exports = require('elasticsearch-browser/elasticsearch.angular.js'); +/** + * Returns dashboard id from URL + * literally looks from id after `dashboard/` string and before `/`, `?` and end of string + * @param url to extract dashboardId from + * input: http://localhost:5601/lib/app/kibana#/dashboard?param1=x¶m2=y¶m3=z + * output: undefined + * input: http://localhost:5601/lib/app/kibana#/dashboard/39292992?param1=x¶m2=y¶m3=z + * output: 39292992 + */ +export function getDashboardIdFromUrl(url: string): string | undefined { + const [, dashboardId] = url.match(/dashboard\/(.*?)(\/|\?|$)/) ?? [ + undefined, // full match + undefined, // group with dashboardId + ]; + return dashboardId ?? undefined; +} diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js index f8e764cbcbebb..5b03b313e4e3e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js @@ -76,6 +76,7 @@ function ContextAppRouteController($routeParams, $scope, $route) { getFilters, setFilters, setAppState, + flushToUrl, } = getState({ defaultStepSize: getServices().uiSettings.get('context:defaultSize'), timeFieldName: indexPattern.timeFieldName, @@ -99,6 +100,7 @@ function ContextAppRouteController($routeParams, $scope, $route) { const [columns, predecessorCount, successorCount] = newValues; if (Array.isArray(columns) && predecessorCount >= 0 && successorCount >= 0) { setAppState({ columns, predecessorCount, successorCount }); + flushToUrl(true); } } ); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts index bf185f78941de..ed59143b163f6 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts @@ -109,7 +109,7 @@ interface GetStateReturn { /** * sync state to URL, used for testing */ - flushToUrl: () => void; + flushToUrl: (replace?: boolean) => void; } const GLOBAL_STATE_URL_KEY = '_g'; const APP_STATE_URL_KEY = '_a'; @@ -205,7 +205,7 @@ export function getState({ } }, // helper function just needed for testing - flushToUrl: () => stateStorage.flush(), + flushToUrl: (replace?: boolean) => stateStorage.flush({ replace }), }; } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts index 3840fd0c2e3be..b7b36ca960167 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts @@ -76,3 +76,30 @@ describe('Test discover state', () => { expect(state.getPreviousAppState()).toEqual(stateA); }); }); + +describe('Test discover state with legacy migration', () => { + test('migration of legacy query ', async () => { + history = createBrowserHistory(); + history.push( + "/#?_a=(query:(query_string:(analyze_wildcard:!t,query:'type:nice%20name:%22yeah%22')))" + ); + state = getState({ + defaultAppState: { index: 'test' }, + history, + }); + expect(state.appStateContainer.getState()).toMatchInlineSnapshot(` + Object { + "index": "test", + "query": Object { + "language": "lucene", + "query": Object { + "query_string": Object { + "analyze_wildcard": true, + "query": "type:nice name:\\"yeah\\"", + }, + }, + }, + } + `); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts index d9e1850cd6a24..2a036f0ac60ad 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts @@ -129,6 +129,11 @@ export function getState({ }); const appStateFromUrl = stateStorage.get(APP_STATE_URL_KEY) as AppState; + + if (appStateFromUrl && appStateFromUrl.query && !appStateFromUrl.query.language) { + appStateFromUrl.query = migrateLegacyQuery(appStateFromUrl.query); + } + let initialAppState = { ...defaultAppState, ...appStateFromUrl, @@ -179,9 +184,6 @@ export function setState(stateContainer: ReduxLikeStateContainer, newS const oldState = stateContainer.getState(); const mergedState = { ...oldState, ...newState }; if (!isEqualState(oldState, mergedState)) { - if (mergedState.query) { - mergedState.query = migrateLegacyQuery(mergedState.query); - } stateContainer.set(mergedState); } } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts index 6f3adc1f4fcce..ad61984a52536 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts @@ -22,9 +22,9 @@ import { i18n } from '@kbn/i18n'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; import { getServices } from '../../kibana_services'; import { - EmbeddableFactory, - ErrorEmbeddable, + EmbeddableFactoryDefinition, Container, + ErrorEmbeddable, } from '../../../../../../../plugins/embeddable/public'; import { TimeRange } from '../../../../../../../plugins/data/public'; @@ -37,28 +37,23 @@ interface StartServices { isEditable: () => boolean; } -export class SearchEmbeddableFactory extends EmbeddableFactory< - SearchInput, - SearchOutput, - SearchEmbeddable -> { +export class SearchEmbeddableFactory + implements EmbeddableFactoryDefinition { public readonly type = SEARCH_EMBEDDABLE_TYPE; private $injector: auto.IInjectorService | null; private getInjector: () => Promise | null; + public readonly savedObjectMetaData = { + name: i18n.translate('kbn.discover.savedSearch.savedObjectName', { + defaultMessage: 'Saved search', + }), + type: 'search', + getIconForSavedObject: () => 'search', + }; constructor( private getStartServices: () => Promise, getInjector: () => Promise ) { - super({ - savedObjectMetaData: { - name: i18n.translate('kbn.discover.savedSearch.savedObjectName', { - defaultMessage: 'Saved search', - }), - type: 'search', - getIconForSavedObject: () => 'search', - }, - }); this.$injector = null; this.getInjector = getInjector; } @@ -67,9 +62,9 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< return false; } - public async isEditable() { + public isEditable = async () => { return (await this.getStartServices()).isEditable(); - } + }; public getDisplayName() { return i18n.translate('kbn.embeddable.search.displayName', { @@ -77,11 +72,11 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< }); } - public async createFromSavedObject( + public createFromSavedObject = async ( savedObjectId: string, input: Partial & { id: string; timeRange: TimeRange }, parent?: Container - ): Promise { + ): Promise => { if (!this.$injector) { this.$injector = await this.getInjector(); } @@ -115,7 +110,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< console.error(e); // eslint-disable-line no-console return new ErrorEmbeddable(e, input, parent); } - } + }; public async create(input: SearchInput) { return new ErrorEmbeddable('Saved searches can only be created from a saved object', input); diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index 2ed7e3d43168c..addc608efd57d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -61,6 +61,7 @@ export interface VisualizeKibanaServices { I18nContext: I18nStart['Context']; setActiveUrl: (newUrl: string) => void; DefaultVisualizationEditor: typeof DefaultEditorController; + createVisEmbeddableFromObject: VisualizationsStart['__LEGACY']['createVisEmbeddableFromObject']; } let services: VisualizeKibanaServices | null = null; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js index d1bf4411cac2a..7c9ab32ab2f72 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js @@ -43,7 +43,7 @@ import { import { createSavedSearchesLoader } from '../../../../../../plugins/discover/public'; const getResolvedResults = deps => { - const { core, data, visualizations } = deps; + const { core, data, visualizations, createVisEmbeddableFromObject } = deps; const results = {}; @@ -60,7 +60,7 @@ const getResolvedResults = deps => { }) .then(vis => { results.vis = vis; - return deps.embeddable.getEmbeddableFactory('visualization').createFromObject(results.vis, { + return createVisEmbeddableFromObject(vis, { timeRange: data.query.timefilter.timefilter.getTime(), filters: data.query.filterManager.getFilters(), }); diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 6d32579f5c541..a14c4a44f1c7c 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -156,6 +156,7 @@ export class VisualizePlugin implements Plugin { I18nContext: coreStart.i18n.Context, setActiveUrl, DefaultVisualizationEditor: DefaultEditorController, + createVisEmbeddableFromObject: visualizations.__LEGACY.createVisEmbeddableFromObject, }; setServices(deps); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.tsx index ca4a9315d6bfb..15e864bfd026d 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.tsx @@ -125,7 +125,7 @@ function DateRangesParamEditor({ - {ranges.map(({ from, to, id }) => { + {ranges.map(({ from, to, id }, index) => { const deleteBtnTitle = i18n.translate( 'visDefaultEditor.controls.dateRanges.removeRangeButtonAriaLabel', { @@ -154,6 +154,7 @@ function DateRangesParamEditor({ placeholder={FROM_PLACEHOLDER} value={from || ''} onChange={ev => onChangeRange(id, 'from', ev.target.value)} + data-test-subj={`visEditorDateRange${index}__from`} /> @@ -168,6 +169,7 @@ function DateRangesParamEditor({ description: 'End of a date range, e.g. From 2018-02-26 *To* 2018-02-28', } )} + data-test-subj={`visEditorDateRange${index}__to`} compressed fullWidth={true} isInvalid={areBothEmpty || !validateDateMath(to)} @@ -203,7 +205,12 @@ function DateRangesParamEditor({ - + { const newData = {}; Object.keys(data).forEach(key => { - if (key !== 'series') { - newData[key] = data[key]; - } else { + if (key === 'xAxisOrderedValues') { + newData[key] = data[key].map(val => { + if (typeof val === 'object') { + return new D3MappableObject(val); + } + return val; + }); + } else if (key === 'series') { newData[key] = data[key].map(seri => { const converter = getFormatService().deserialize(seri.format); const zConverter = getFormatService().deserialize(seri.zFormat); @@ -67,12 +92,17 @@ export class Data { const newVal = _.clone(val); newVal.extraMetrics = val.extraMetrics; newVal.series = val.series || seri.label; + if (typeof newVal.x === 'object') { + newVal.x = new D3MappableObject(newVal.x); + } return newVal; }), yAxisFormatter: val => converter.convert(val), zAxisFormatter: val => zConverter.convert(val), }; }); + } else { + newData[key] = data[key]; } }); diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index 106dbcd9f8ab2..ad4aa97d8ea7a 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -76,24 +76,33 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { load({ deps: [ + {{#each sharedJsDepFilenames}} + '{{../regularBundlePath}}/kbn-ui-shared-deps/{{this}}', + {{/each}} + ], + urls: [ { deps: [ - '{{dllBundlePath}}/vendors_runtime.bundle.dll.js' + '{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedJsFilename}}', + { + deps: [ + '{{dllBundlePath}}/vendors_runtime.bundle.dll.js' + ], + urls: [ + {{#each dllJsChunks}} + '{{this}}', + {{/each}} + ] + }, + '{{regularBundlePath}}/commons.bundle.js', ], urls: [ - {{#each dllJsChunks}} + '{{regularBundlePath}}/{{appId}}.bundle.js', + {{#each styleSheetPaths}} '{{this}}', {{/each}} ] - }, - '{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedDepsFilename}}', - '{{regularBundlePath}}/commons.bundle.js', - ], - urls: [ - '{{regularBundlePath}}/{{appId}}.bundle.js', - {{#each styleSheetPaths}} - '{{this}}', - {{/each}}, + } ] }); }; diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 99560b0bf653f..0912d8683fc48 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -135,7 +135,8 @@ export function uiRenderMixin(kbnServer, server, config) { dllBundlePath, dllJsChunks, styleSheetPaths, - sharedDepsFilename: UiSharedDeps.distFilename, + sharedJsFilename: UiSharedDeps.jsFilename, + sharedJsDepFilenames: UiSharedDeps.jsDepFilenames, darkMode, }, }); diff --git a/src/plugins/advanced_settings/public/management_app/index.tsx b/src/plugins/advanced_settings/public/management_app/index.tsx deleted file mode 100644 index 53b8f9983aa27..0000000000000 --- a/src/plugins/advanced_settings/public/management_app/index.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import { HashRouter, Switch, Route } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n/react'; -import { AdvancedSettings } from './advanced_settings'; -import { ManagementSetup } from '../../../management/public'; -import { StartServicesAccessor } from '../../../../core/public'; -import { ComponentRegistry } from '../types'; - -const title = i18n.translate('advancedSettings.advancedSettingsLabel', { - defaultMessage: 'Advanced Settings', -}); -const crumb = [{ text: title }]; - -const readOnlyBadge = { - text: i18n.translate('advancedSettings.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - tooltip: i18n.translate('advancedSettings.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save advanced settings', - }), - iconType: 'glasses', -}; - -export async function registerAdvSettingsMgmntApp({ - management, - getStartServices, - componentRegistry, -}: { - management: ManagementSetup; - getStartServices: StartServicesAccessor; - componentRegistry: ComponentRegistry['start']; -}) { - const kibanaSection = management.sections.getSection('kibana'); - if (!kibanaSection) { - throw new Error('`kibana` management section not found.'); - } - - const advancedSettingsManagementApp = kibanaSection.registerApp({ - id: 'settings', - title, - order: 20, - async mount(params) { - params.setBreadcrumbs(crumb); - const [ - { uiSettings, notifications, docLinks, application, chrome }, - ] = await getStartServices(); - - const canSave = application.capabilities.advancedSettings.save as boolean; - - if (!canSave) { - chrome.setBadge(readOnlyBadge); - } - - ReactDOM.render( - - - - - - - - - , - params.element - ); - return () => { - ReactDOM.unmountComponentAtNode(params.element); - }; - }, - }); - const [{ application }] = await getStartServices(); - if (!application.capabilities.management.kibana.settings) { - advancedSettingsManagementApp.disable(); - } -} diff --git a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx new file mode 100644 index 0000000000000..df44ea45e9d01 --- /dev/null +++ b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { HashRouter, Switch, Route } from 'react-router-dom'; + +import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n/react'; +import { StartServicesAccessor } from 'src/core/public'; + +import { AdvancedSettings } from './advanced_settings'; +import { ManagementAppMountParams } from '../../../management/public'; +import { ComponentRegistry } from '../types'; + +const title = i18n.translate('advancedSettings.advancedSettingsLabel', { + defaultMessage: 'Advanced Settings', +}); +const crumb = [{ text: title }]; + +const readOnlyBadge = { + text: i18n.translate('advancedSettings.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + tooltip: i18n.translate('advancedSettings.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save advanced settings', + }), + iconType: 'glasses', +}; + +export async function mountManagementSection( + getStartServices: StartServicesAccessor, + params: ManagementAppMountParams, + componentRegistry: ComponentRegistry['start'] +) { + params.setBreadcrumbs(crumb); + const [{ uiSettings, notifications, docLinks, application, chrome }] = await getStartServices(); + + const canSave = application.capabilities.advancedSettings.save as boolean; + + if (!canSave) { + chrome.setBadge(readOnlyBadge); + } + + ReactDOM.render( + + + + + + + + + , + params.element + ); + return () => { + ReactDOM.unmountComponentAtNode(params.element); + }; +} diff --git a/src/plugins/advanced_settings/public/plugin.ts b/src/plugins/advanced_settings/public/plugin.ts index e9472fbdee0e6..04eeff1e1f3ce 100644 --- a/src/plugins/advanced_settings/public/plugin.ts +++ b/src/plugins/advanced_settings/public/plugin.ts @@ -16,21 +16,37 @@ * specific language governing permissions and limitations * under the License. */ - +import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { ManagementApp } from '../../management/public'; import { ComponentRegistry } from './component_registry'; import { AdvancedSettingsSetup, AdvancedSettingsStart, AdvancedSettingsPluginSetup } from './types'; -import { registerAdvSettingsMgmntApp } from './management_app'; const component = new ComponentRegistry(); +const title = i18n.translate('advancedSettings.advancedSettingsLabel', { + defaultMessage: 'Advanced Settings', +}); + export class AdvancedSettingsPlugin implements Plugin { + private managementApp?: ManagementApp; public setup(core: CoreSetup, { management }: AdvancedSettingsPluginSetup) { - registerAdvSettingsMgmntApp({ - management, - getStartServices: core.getStartServices, - componentRegistry: component.start, + const kibanaSection = management.sections.getSection('kibana'); + if (!kibanaSection) { + throw new Error('`kibana` management section not found.'); + } + + this.managementApp = kibanaSection.registerApp({ + id: 'settings', + title, + order: 20, + async mount(params) { + const { mountManagementSection } = await import( + './management_app/mount_management_section' + ); + return mountManagementSection(core.getStartServices, params, component.start); + }, }); return { @@ -39,6 +55,10 @@ export class AdvancedSettingsPlugin } public start(core: CoreStart) { + if (!core.application.capabilities.management.kibana.settings) { + this.managementApp!.disable(); + } + return { component: component.start, }; diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts index 548366c63aa05..dde793d9b9691 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts @@ -39,7 +39,7 @@ export const useSendCurrentRequestToES = () => { const requests = await editor.getRequestsInRange(); if (!requests.length) { notifications.toasts.add( - i18n.translate('console.notification.noReqeustSelectedTitle', { + i18n.translate('console.notification.error.noRequestSelectedTitle', { defaultMessage: 'No request selected. Select a request by placing the cursor inside it.', }) @@ -55,7 +55,16 @@ export const useSendCurrentRequestToES = () => { const results = await sendRequestToES({ requests }); results.forEach(({ request: { path, method, data } }) => { - history.addToHistory(path, method, data); + try { + history.addToHistory(path, method, data); + } catch (e) { + // Best effort, but notify the user. + notifications.toasts.addError(e, { + title: i18n.translate('console.notification.error.couldNotSaveRequestTitle', { + defaultMessage: 'Could not save request to history.', + }), + }); + } }); const { polling } = settings.toJSON(); @@ -85,7 +94,7 @@ export const useSendCurrentRequestToES = () => { payload: undefined, }); notifications.toasts.addError(e, { - title: i18n.translate('console.notification.unknownRequestErrorTitle', { + title: i18n.translate('console.notification.error.unknownErrorTitle', { defaultMessage: 'Unknown Request Error', }), }); diff --git a/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx b/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx index 22cf854a46623..e9696938b8629 100644 --- a/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx +++ b/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { isErrorEmbeddable, EmbeddableFactory } from '../embeddable_plugin'; +import { isErrorEmbeddable } from '../embeddable_plugin'; import { ExpandPanelAction } from './expand_panel_action'; import { DashboardContainer } from '../embeddable'; import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers'; @@ -29,11 +29,16 @@ import { ContactCardEmbeddableOutput, } from '../embeddable_plugin_test_samples'; -const embeddableFactories = new Map(); -embeddableFactories.set( +// eslint-disable-next-line +import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; + +const { setup, doStart } = embeddablePluginMock.createInstance(); + +setup.registerEmbeddableFactory( CONTACT_CARD_EMBEDDABLE, - new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any) + new ContactCardEmbeddableFactory((() => null) as any, {} as any) ); +const start = doStart(); let container: DashboardContainer; let embeddable: ContactCardEmbeddable; @@ -43,9 +48,7 @@ beforeEach(async () => { ExitFullScreenButton: () => null, SavedObjectFinder: () => null, application: {} as any, - embeddable: { - getEmbeddableFactory: (id: string) => embeddableFactories.get(id)!, - } as any, + embeddable: start, inspector: {} as any, notifications: {} as any, overlays: {} as any, diff --git a/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx b/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx index 69346dc8c118a..2252928f46f6a 100644 --- a/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx +++ b/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { isErrorEmbeddable, EmbeddableFactory } from '../embeddable_plugin'; +import { isErrorEmbeddable } from '../embeddable_plugin'; import { ReplacePanelAction } from './replace_panel_action'; import { DashboardContainer } from '../embeddable'; import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers'; @@ -30,12 +30,15 @@ import { import { coreMock } from '../../../../core/public/mocks'; import { CoreStart } from 'kibana/public'; -const embeddableFactories = new Map(); -embeddableFactories.set( +// eslint-disable-next-line +import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; + +const { setup, doStart } = embeddablePluginMock.createInstance(); +setup.registerEmbeddableFactory( CONTACT_CARD_EMBEDDABLE, - new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any) + new ContactCardEmbeddableFactory((() => null) as any, {} as any) ); -const getEmbeddableFactories = () => embeddableFactories.values(); +const start = doStart(); let container: DashboardContainer; let embeddable: ContactCardEmbeddable; @@ -46,9 +49,7 @@ beforeEach(async () => { ExitFullScreenButton: () => null, SavedObjectFinder: () => null, application: {} as any, - embeddable: { - getEmbeddableFactory: (id: string) => embeddableFactories.get(id)!, - } as any, + embeddable: start, inspector: {} as any, notifications: {} as any, overlays: coreStart.overlays, @@ -87,7 +88,7 @@ test('Executes the replace panel action', async () => { coreStart, SavedObjectFinder, notifications, - getEmbeddableFactories + start.getEmbeddableFactories ); action.execute({ embeddable }); }); @@ -99,7 +100,7 @@ test('Is not compatible when embeddable is not in a dashboard container', async coreStart, SavedObjectFinder, notifications, - getEmbeddableFactories + start.getEmbeddableFactories ); expect( await action.isCompatible({ @@ -118,7 +119,7 @@ test('Execute throws an error when called with an embeddable not in a parent', a coreStart, SavedObjectFinder, notifications, - getEmbeddableFactories + start.getEmbeddableFactories ); async function check() { await action.execute({ embeddable: container }); @@ -133,7 +134,7 @@ test('Returns title', async () => { coreStart, SavedObjectFinder, notifications, - getEmbeddableFactories + start.getEmbeddableFactories ); expect(action.getDisplayName({ embeddable })).toBeDefined(); }); @@ -145,7 +146,7 @@ test('Returns an icon', async () => { coreStart, SavedObjectFinder, notifications, - getEmbeddableFactories + start.getEmbeddableFactories ); expect(action.getIconType({ embeddable })).toBeDefined(); }); diff --git a/src/plugins/dashboard/public/embeddable/dashboard_container.test.tsx b/src/plugins/dashboard/public/embeddable/dashboard_container.test.tsx index 770c46c62e42f..6a734cb68fd9c 100644 --- a/src/plugins/dashboard/public/embeddable/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/embeddable/dashboard_container.test.tsx @@ -20,7 +20,7 @@ // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { nextTick } from 'test_utils/enzyme_helpers'; -import { isErrorEmbeddable, ViewMode, EmbeddableFactory } from '../embeddable_plugin'; +import { isErrorEmbeddable, ViewMode } from '../embeddable_plugin'; import { DashboardContainer, DashboardContainerOptions } from './dashboard_container'; import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers'; import { @@ -30,14 +30,12 @@ import { ContactCardEmbeddable, ContactCardEmbeddableOutput, } from '../embeddable_plugin_test_samples'; +// eslint-disable-next-line +import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; const options: DashboardContainerOptions = { application: {} as any, - embeddable: { - getTriggerCompatibleActions: (() => []) as any, - getEmbeddableFactories: (() => []) as any, - getEmbeddableFactory: undefined as any, - } as any, + embeddable: {} as any, notifications: {} as any, overlays: {} as any, inspector: {} as any, @@ -47,12 +45,12 @@ const options: DashboardContainerOptions = { }; beforeEach(() => { - const embeddableFactories = new Map(); - embeddableFactories.set( + const { setup, doStart } = embeddablePluginMock.createInstance(); + setup.registerEmbeddableFactory( CONTACT_CARD_EMBEDDABLE, - new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any) + new ContactCardEmbeddableFactory((() => null) as any, {} as any) ); - options.embeddable.getEmbeddableFactory = (id: string) => embeddableFactories.get(id) as any; + options.embeddable = doStart(); }); test('DashboardContainer initializes embeddables', async done => { diff --git a/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx b/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx index 0fa62fc875603..9ff48cb45adfd 100644 --- a/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx +++ b/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx @@ -23,7 +23,7 @@ import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { CoreStart } from '../../../../core/public'; import { ContainerOutput, - EmbeddableFactory, + EmbeddableFactoryDefinition, ErrorEmbeddable, Container, } from '../embeddable_plugin'; @@ -43,27 +43,24 @@ interface StartServices { uiActions: UiActionsStart; } -export class DashboardContainerFactory extends EmbeddableFactory< - DashboardContainerInput, - ContainerOutput -> { +export class DashboardContainerFactory + implements + EmbeddableFactoryDefinition { public readonly isContainerType = true; public readonly type = DASHBOARD_CONTAINER_TYPE; - constructor(private readonly getStartServices: () => Promise) { - super(); - } + constructor(private readonly getStartServices: () => Promise) {} - public async isEditable() { + public isEditable = async () => { const { capabilities } = await this.getStartServices(); return !!capabilities.createNew && !!capabilities.showWriteControls; - } + }; - public getDisplayName() { + public readonly getDisplayName = () => { return i18n.translate('dashboard.factory.displayName', { defaultMessage: 'dashboard', }); - } + }; public getDefaultInput(): Partial { return { @@ -73,11 +70,11 @@ export class DashboardContainerFactory extends EmbeddableFactory< }; } - public async create( + public create = async ( initialInput: DashboardContainerInput, parent?: Container - ): Promise { + ): Promise => { const services = await this.getStartServices(); return new DashboardContainer(initialInput, services, parent); - } + }; } diff --git a/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx b/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx index 0f1b9c6dc9307..a946c21765311 100644 --- a/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx +++ b/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx @@ -23,7 +23,6 @@ import sizeMe from 'react-sizeme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { skip } from 'rxjs/operators'; -import { EmbeddableFactory } from '../../embeddable_plugin'; import { DashboardGrid, DashboardGridProps } from './dashboard_grid'; import { DashboardContainer, DashboardContainerOptions } from '../dashboard_container'; import { getSampleDashboardInput } from '../../test_helpers'; @@ -32,16 +31,20 @@ import { ContactCardEmbeddableFactory, } from '../../embeddable_plugin_test_samples'; import { KibanaContextProvider } from '../../../../kibana_react/public'; +// eslint-disable-next-line +import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; let dashboardContainer: DashboardContainer | undefined; function prepare(props?: Partial) { - const embeddableFactories = new Map(); - embeddableFactories.set( + const { setup, doStart } = embeddablePluginMock.createInstance(); + setup.registerEmbeddableFactory( CONTACT_CARD_EMBEDDABLE, - new ContactCardEmbeddableFactory({} as any, (() => {}) as any, {} as any) + new ContactCardEmbeddableFactory((() => null) as any, {} as any) ); - const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const start = doStart(); + + const getEmbeddableFactory = start.getEmbeddableFactory; const initialInput = getSampleDashboardInput({ panels: { '1': { @@ -60,7 +63,7 @@ function prepare(props?: Partial) { application: {} as any, embeddable: { getTriggerCompatibleActions: (() => []) as any, - getEmbeddableFactories: (() => []) as any, + getEmbeddableFactories: start.getEmbeddableFactories, getEmbeddableFactory, } as any, notifications: {} as any, diff --git a/src/plugins/dashboard/public/embeddable/viewport/dashboard_viewport.test.tsx b/src/plugins/dashboard/public/embeddable/viewport/dashboard_viewport.test.tsx index e3d9b8552f060..be4d2e3851f11 100644 --- a/src/plugins/dashboard/public/embeddable/viewport/dashboard_viewport.test.tsx +++ b/src/plugins/dashboard/public/embeddable/viewport/dashboard_viewport.test.tsx @@ -24,7 +24,6 @@ import { skip } from 'rxjs/operators'; import { mount } from 'enzyme'; import { I18nProvider } from '@kbn/i18n/react'; import { nextTick } from 'test_utils/enzyme_helpers'; -import { EmbeddableFactory } from '../../embeddable_plugin'; import { DashboardViewport, DashboardViewportProps } from './dashboard_viewport'; import { DashboardContainer, DashboardContainerOptions } from '../dashboard_container'; import { getSampleDashboardInput } from '../../test_helpers'; @@ -33,6 +32,8 @@ import { ContactCardEmbeddableFactory, } from '../../embeddable_plugin_test_samples'; import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; +// eslint-disable-next-line +import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; let dashboardContainer: DashboardContainer | undefined; @@ -41,18 +42,19 @@ const ExitFullScreenButton = () =>
function getProps( props?: Partial ): { props: DashboardViewportProps; options: DashboardContainerOptions } { - const embeddableFactories = new Map(); - embeddableFactories.set( + const { setup, doStart } = embeddablePluginMock.createInstance(); + setup.registerEmbeddableFactory( CONTACT_CARD_EMBEDDABLE, - new ContactCardEmbeddableFactory({}, (() => null) as any, {} as any) + new ContactCardEmbeddableFactory((() => null) as any, {} as any) ); + const start = doStart(); const options: DashboardContainerOptions = { application: {} as any, embeddable: { getTriggerCompatibleActions: (() => []) as any, - getEmbeddableFactories: (() => []) as any, - getEmbeddableFactory: (id: string) => embeddableFactories.get(id), + getEmbeddableFactories: start.getEmbeddableFactories, + getEmbeddableFactory: start.getEmbeddableFactory, } as any, notifications: {} as any, overlays: {} as any, diff --git a/src/plugins/dashboard/public/tests/dashboard_container.test.tsx b/src/plugins/dashboard/public/tests/dashboard_container.test.tsx index a81d80b440e04..1c72ad34e5446 100644 --- a/src/plugins/dashboard/public/tests/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/tests/dashboard_container.test.tsx @@ -52,7 +52,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => { uiActionsSetup.attachAction(CONTEXT_MENU_TRIGGER, editModeAction); setup.registerEmbeddableFactory( CONTACT_CARD_EMBEDDABLE, - new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any) + new ContactCardEmbeddableFactory((() => null) as any, {} as any) ); const start = doStart(); diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.test.ts b/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.test.ts index 0c02b02a25af0..ef6eaa196b06a 100644 --- a/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.test.ts +++ b/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.test.ts @@ -46,6 +46,10 @@ describe('parseInterval', () => { validateDuration(parseInterval('5m'), 'm', 5); }); + test('should correctly parse 500m interval', () => { + validateDuration(parseInterval('500m'), 'm', 500); + }); + test('should correctly parse 250ms interval', () => { validateDuration(parseInterval('250ms'), 'ms', 250); }); diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.ts b/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.ts index ef1d89e400b72..857c8594720ee 100644 --- a/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.ts +++ b/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.ts @@ -49,6 +49,13 @@ export function parseInterval(interval: string): moment.Duration | null { u => Math.abs(duration.as(u)) >= 1 ) as unitOfTime.Base; + // however if we do this fhe other way around it will also fail + // go from 500m to hours as this will result in infinite number (dividing 500/60 = 8.3*) + // so we can only do this if we are changing to smaller units + if (dateMath.units.indexOf(selectedUnit as any) < dateMath.units.indexOf(unit as any)) { + return duration; + } + return moment.duration(duration.as(selectedUnit), selectedUnit); } catch (e) { return null; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index eca74af4ec253..23275fbe8e8f0 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -38,6 +38,7 @@ export { EmbeddableChildPanel, EmbeddableChildPanelProps, EmbeddableContext, + EmbeddableFactoryDefinition, EmbeddableFactory, EmbeddableFactoryNotFoundError, EmbeddableFactoryRenderer, diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index 9aeaf34f3311b..ce733bba6dda5 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx @@ -18,14 +18,14 @@ */ import { EditPanelAction } from './edit_panel_action'; -import { EmbeddableFactory, Embeddable, EmbeddableInput } from '../embeddables'; +import { Embeddable, EmbeddableInput } from '../embeddables'; import { ViewMode } from '../types'; import { ContactCardEmbeddable } from '../test_samples'; -import { EmbeddableStart } from '../../plugin'; +import { embeddablePluginMock } from '../../mocks'; -const embeddableFactories = new Map(); -const getFactory = ((id: string) => - embeddableFactories.get(id)) as EmbeddableStart['getEmbeddableFactory']; +const { doStart } = embeddablePluginMock.createInstance(); +const start = doStart(); +const getFactory = start.getEmbeddableFactory; class EditableEmbeddable extends Embeddable { public readonly type = 'EDITABLE_EMBEDDABLE'; @@ -83,9 +83,7 @@ test('is not compatible when edit url is not available', async () => { }); test('is not visible when edit url is available but in view mode', async () => { - embeddableFactories.clear(); - const action = new EditPanelAction((type => - embeddableFactories.get(type)) as EmbeddableStart['getEmbeddableFactory']); + const action = new EditPanelAction(getFactory); expect( await action.isCompatible({ embeddable: new EditableEmbeddable( @@ -100,9 +98,7 @@ test('is not visible when edit url is available but in view mode', async () => { }); test('is not compatible when edit url is available, in edit mode, but not editable', async () => { - embeddableFactories.clear(); - const action = new EditPanelAction((type => - embeddableFactories.get(type)) as EmbeddableStart['getEmbeddableFactory']); + const action = new EditPanelAction(getFactory); expect( await action.isCompatible({ embeddable: new EditableEmbeddable( diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx index 07915ce59e6ca..9e47da5cea032 100644 --- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { nextTick } from 'test_utils/enzyme_helpers'; import { EmbeddableChildPanel } from './embeddable_child_panel'; -import { EmbeddableFactory } from '../embeddables'; import { CONTACT_CARD_EMBEDDABLE } from '../test_samples/embeddables/contact_card/contact_card_embeddable_factory'; import { SlowContactCardEmbeddableFactory } from '../test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory'; import { HelloWorldContainer } from '../test_samples/embeddables/hello_world_container'; @@ -32,16 +31,17 @@ import { // eslint-disable-next-line import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks'; import { mount } from 'enzyme'; +import { embeddablePluginMock } from '../../mocks'; test('EmbeddableChildPanel renders an embeddable when it is done loading', async () => { const inspector = inspectorPluginMock.createStartContract(); - - const embeddableFactories = new Map(); - embeddableFactories.set( + const { setup, doStart } = embeddablePluginMock.createInstance(); + setup.registerEmbeddableFactory( CONTACT_CARD_EMBEDDABLE, new SlowContactCardEmbeddableFactory({ execAction: (() => null) as any }) ); - const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const start = doStart(); + const getEmbeddableFactory = start.getEmbeddableFactory; const container = new HelloWorldContainer({ id: 'hello', panels: {} }, { getEmbeddableFactory, @@ -63,8 +63,8 @@ test('EmbeddableChildPanel renders an embeddable when it is done loading', async container={container} embeddableId={newEmbeddable.id} getActions={() => Promise.resolve([])} - getAllEmbeddableFactories={(() => []) as any} - getEmbeddableFactory={(() => undefined) as any} + getAllEmbeddableFactories={start.getEmbeddableFactories} + getEmbeddableFactory={getEmbeddableFactory} notifications={{} as any} overlays={{} as any} inspector={inspector} diff --git a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts new file mode 100644 index 0000000000000..570a78fc41ea9 --- /dev/null +++ b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { SavedObjectAttributes } from 'kibana/public'; +import { EmbeddableFactoryDefinition } from './embeddable_factory_definition'; +import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable'; +import { EmbeddableFactory } from './embeddable_factory'; +import { IContainer } from '..'; + +export const defaultEmbeddableFactoryProvider = < + I extends EmbeddableInput = EmbeddableInput, + O extends EmbeddableOutput = EmbeddableOutput, + E extends IEmbeddable = IEmbeddable, + T extends SavedObjectAttributes = SavedObjectAttributes +>( + def: EmbeddableFactoryDefinition +): EmbeddableFactory => { + const factory: EmbeddableFactory = { + isContainerType: def.isContainerType ?? false, + canCreateNew: def.canCreateNew ? def.canCreateNew.bind(def) : () => true, + getDefaultInput: def.getDefaultInput ? def.getDefaultInput.bind(def) : () => ({}), + getExplicitInput: def.getExplicitInput + ? def.getExplicitInput.bind(def) + : () => Promise.resolve({}), + createFromSavedObject: + def.createFromSavedObject ?? + ((savedObjectId: string, input: Partial, parent?: IContainer) => { + throw new Error(`Creation from saved object not supported by type ${def.type}`); + }), + create: def.create.bind(def), + type: def.type, + isEditable: def.isEditable.bind(def), + getDisplayName: def.getDisplayName.bind(def), + savedObjectMetaData: def.savedObjectMetaData, + }; + return factory; +}; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index eb10c16806640..a135484ff61be 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -158,6 +158,8 @@ export abstract class Embeddable< */ public destroy(): void { this.destoyed = true; + this.input$.complete(); + this.output$.complete(); if (this.parentSubscription) { this.parentSubscription.unsubscribe(); } diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts index 81f7f35c900c9..7949b6fb8ba27 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts @@ -22,32 +22,21 @@ import { SavedObjectMetaData } from '../../../../saved_objects/public'; import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable'; import { ErrorEmbeddable } from './error_embeddable'; import { IContainer } from '../containers/i_container'; +import { PropertySpec } from '../types'; export interface EmbeddableInstanceConfiguration { id: string; savedObjectId?: string; } -export interface PropertySpec { - displayName: string; - accessPath: string; - id: string; - description: string; - value?: string; -} - export interface OutputSpec { [key: string]: PropertySpec; } -export interface EmbeddableFactoryOptions { - savedObjectMetaData?: SavedObjectMetaData; -} - /** - * The EmbeddableFactory creates and initializes an embeddable instance + * EmbeddableFactories create and initialize an embeddable instance */ -export abstract class EmbeddableFactory< +export interface EmbeddableFactory< TEmbeddableInput extends EmbeddableInput = EmbeddableInput, TEmbeddableOutput extends EmbeddableOutput = EmbeddableOutput, TEmbeddable extends IEmbeddable = IEmbeddable< @@ -58,9 +47,15 @@ export abstract class EmbeddableFactory< > { // A unique identified for this factory, which will be used to map an embeddable spec to // a factory that can generate an instance of it. - public abstract readonly type: string; + readonly type: string; + + /** + * Returns whether the current user should be allowed to edit this type of + * embeddable. Most of the time this should be based off the capabilities service, hence it's async. + */ + readonly isEditable: () => Promise; - public readonly savedObjectMetaData?: SavedObjectMetaData; + readonly savedObjectMetaData?: SavedObjectMetaData; /** * True if is this factory create embeddables that are Containers. Used in the add panel to @@ -68,31 +63,19 @@ export abstract class EmbeddableFactory< * supported right now, but once nested containers are officially supported we can probably get * rid of this interface. */ - public readonly isContainerType: boolean = false; - - constructor({ savedObjectMetaData }: EmbeddableFactoryOptions = {}) { - this.savedObjectMetaData = savedObjectMetaData; - } - - /** - * Returns whether the current user should be allowed to edit this type of - * embeddable. Most of the time this should be based off the capabilities service, hence it's async. - */ - public abstract async isEditable(): Promise; + readonly isContainerType: boolean; /** * Returns a display name for this type of embeddable. Used in "Create new... " options * in the add panel for containers. */ - public abstract getDisplayName(): string; + getDisplayName(): string; /** * If false, this type of embeddable can't be created with the "createNew" functionality. Instead, * use createFromSavedObject, where an existing saved object must first exist. */ - public canCreateNew() { - return true; - } + canCreateNew(): boolean; /** * Can be used to get any default input, to be passed in to during the creation process. Default @@ -100,18 +83,14 @@ export abstract class EmbeddableFactory< * default input parameters. * @param partial */ - public getDefaultInput(partial: Partial): Partial { - return {}; - } + getDefaultInput(partial: Partial): Partial; /** * Can be used to request explicit input from the user, to be passed in to `EmbeddableFactory:create`. * Explicit input is stored on the parent container for this embeddable. It overrides any inherited * input passed down from the parent container. */ - public async getExplicitInput(): Promise> { - return {}; - } + getExplicitInput(): Promise>; /** * Creates a new embeddable instance based off the saved object id. @@ -120,13 +99,11 @@ export abstract class EmbeddableFactory< * range of the parent container. * @param parent */ - public createFromSavedObject( + createFromSavedObject( savedObjectId: string, input: Partial, parent?: IContainer - ): Promise { - throw new Error(`Creation from saved object not supported by type ${this.type}`); - } + ): Promise; /** * Resolves to undefined if a new Embeddable cannot be directly created and the user will instead be redirected @@ -134,7 +111,7 @@ export abstract class EmbeddableFactory< * * This will likely change in future iterations when we improve in place editing capabilities. */ - public abstract create( + create( initialInput: TEmbeddableInput, parent?: IContainer ): Promise; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts new file mode 100644 index 0000000000000..b8985f7311ea9 --- /dev/null +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectAttributes } from 'kibana/server'; +import { IEmbeddable } from './i_embeddable'; +import { EmbeddableFactory } from './embeddable_factory'; +import { EmbeddableInput, EmbeddableOutput } from '..'; + +export type EmbeddableFactoryDefinition< + I extends EmbeddableInput = EmbeddableInput, + O extends EmbeddableOutput = EmbeddableOutput, + E extends IEmbeddable = IEmbeddable, + T extends SavedObjectAttributes = SavedObjectAttributes +> = + // Required parameters + Pick, 'create' | 'type' | 'isEditable' | 'getDisplayName'> & + // Optional parameters + Partial< + Pick< + EmbeddableFactory, + | 'createFromSavedObject' + | 'isContainerType' + | 'getExplicitInput' + | 'savedObjectMetaData' + | 'canCreateNew' + | 'getDefaultInput' + > + >; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx index 51b83ea0ecaa3..e27045495af5b 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx @@ -21,22 +21,22 @@ import { HELLO_WORLD_EMBEDDABLE, HelloWorldEmbeddableFactory, } from '../../../../../../examples/embeddable_examples/public'; -import { EmbeddableFactory } from './embeddable_factory'; import { EmbeddableFactoryRenderer } from './embeddable_factory_renderer'; import { mount } from 'enzyme'; import { nextTick } from 'test_utils/enzyme_helpers'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; -import { EmbeddableStart } from '../../plugin'; +import { embeddablePluginMock } from '../../mocks'; test('EmbeddableFactoryRenderer renders an embeddable', async () => { - const embeddableFactories = new Map(); - embeddableFactories.set(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory()); - const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const { setup, doStart } = embeddablePluginMock.createInstance(); + setup.registerEmbeddableFactory(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory()); + + const getEmbeddableFactory = doStart().getEmbeddableFactory; const component = mount( diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts index 2175c3a59aa58..4d6ab37a50c05 100644 --- a/src/plugins/embeddable/public/lib/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/embeddables/index.ts @@ -18,11 +18,9 @@ */ export { EmbeddableOutput, EmbeddableInput, IEmbeddable } from './i_embeddable'; export { Embeddable } from './embeddable'; -export { - EmbeddableInstanceConfiguration, - EmbeddableFactory, - OutputSpec, -} from './embeddable_factory'; +export * from './embeddable_factory'; +export * from './embeddable_factory_definition'; +export * from './default_embeddable_factory_provider'; export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; export { withEmbeddableSubscription } from './with_subscription'; export { EmbeddableFactoryRenderer } from './embeddable_factory_renderer'; diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx index 757d4e6bfddef..649677dc67c7d 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -27,7 +27,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { CONTEXT_MENU_TRIGGER } from '../triggers'; import { Action, UiActionsStart, ActionType } from 'src/plugins/ui_actions/public'; import { Trigger, ViewMode } from '../types'; -import { EmbeddableFactory, isErrorEmbeddable } from '../embeddables'; +import { isErrorEmbeddable } from '../embeddables'; import { EmbeddablePanel } from './embeddable_panel'; import { createEditModeAction } from '../test_samples/actions'; import { @@ -43,26 +43,25 @@ import { // eslint-disable-next-line import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks'; import { EuiBadge } from '@elastic/eui'; +import { embeddablePluginMock } from '../../mocks'; const actionRegistry = new Map>(); const triggerRegistry = new Map(); -const embeddableFactories = new Map(); -const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); + +const { setup, doStart } = embeddablePluginMock.createInstance(); const editModeAction = createEditModeAction(); const trigger: Trigger = { id: CONTEXT_MENU_TRIGGER, }; -const embeddableFactory = new ContactCardEmbeddableFactory( - {} as any, - (() => null) as any, - {} as any -); +const embeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any); actionRegistry.set(editModeAction.id, editModeAction); triggerRegistry.set(trigger.id, trigger); -embeddableFactories.set(embeddableFactory.type, embeddableFactory); +setup.registerEmbeddableFactory(embeddableFactory.type, embeddableFactory); +const start = doStart(); +const getEmbeddableFactory = start.getEmbeddableFactory; test('HelloWorldContainer initializes embeddables', async done => { const container = new HelloWorldContainer( { @@ -157,8 +156,8 @@ test('HelloWorldContainer in view mode hides edit mode actions', async () => { Promise.resolve([])} - getAllEmbeddableFactories={(() => []) as any} - getEmbeddableFactory={(() => undefined) as any} + getAllEmbeddableFactories={start.getEmbeddableFactories} + getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} overlays={{} as any} inspector={inspector} @@ -195,8 +194,8 @@ const renderInEditModeAndOpenContextMenu = async ( []) as any} - getEmbeddableFactory={(() => undefined) as any} + getAllEmbeddableFactories={start.getEmbeddableFactories} + getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} overlays={{} as any} inspector={inspector} @@ -293,8 +292,8 @@ test('HelloWorldContainer in edit mode shows edit mode actions', async () => { Promise.resolve([])} - getAllEmbeddableFactories={(() => []) as any} - getEmbeddableFactory={(() => undefined) as any} + getAllEmbeddableFactories={start.getEmbeddableFactories} + getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} overlays={{} as any} inspector={inspector} @@ -355,8 +354,8 @@ test('Updates when hidePanelTitles is toggled', async () => { Promise.resolve([])} - getAllEmbeddableFactories={(() => []) as any} - getEmbeddableFactory={(() => undefined) as any} + getAllEmbeddableFactories={start.getEmbeddableFactories} + getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} overlays={{} as any} inspector={inspector} @@ -407,8 +406,8 @@ test('Check when hide header option is false', async () => { Promise.resolve([])} - getAllEmbeddableFactories={(() => []) as any} - getEmbeddableFactory={(() => undefined) as any} + getAllEmbeddableFactories={start.getEmbeddableFactories} + getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} overlays={{} as any} inspector={inspector} @@ -444,8 +443,8 @@ test('Check when hide header option is true', async () => { Promise.resolve([])} - getAllEmbeddableFactories={(() => []) as any} - getEmbeddableFactory={(() => undefined) as any} + getAllEmbeddableFactories={start.getEmbeddableFactories} + getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} overlays={{} as any} inspector={inspector} diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx index 8ee8c8dad9df3..74b08535bf27a 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx @@ -19,7 +19,6 @@ import { ViewMode, EmbeddableOutput, isErrorEmbeddable } from '../../../../'; import { AddPanelAction } from './add_panel_action'; -import { EmbeddableFactory } from '../../../../embeddables'; import { FILTERABLE_EMBEDDABLE, FilterableEmbeddable, @@ -31,11 +30,12 @@ import { FilterableContainer } from '../../../../test_samples/embeddables/filter import { coreMock } from '../../../../../../../../core/public/mocks'; import { ContactCardEmbeddable } from '../../../../test_samples'; import { esFilters, Filter } from '../../../../../../../../plugins/data/public'; -import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; +import { EmbeddableStart } from '../../../../../plugin'; +import { embeddablePluginMock } from '../../../../../mocks'; -const embeddableFactories = new Map(); -embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); -const getFactory = (id: string) => embeddableFactories.get(id); +const { setup, doStart } = embeddablePluginMock.createInstance(); +setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); +const getFactory = doStart().getEmbeddableFactory; let container: FilterableContainer; let embeddable: FilterableEmbeddable; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx index 2fa21e40ca0f0..282b0f05891e0 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx @@ -31,7 +31,7 @@ import { ReactWrapper } from 'enzyme'; import { coreMock } from '../../../../../../../../core/public/mocks'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; -import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; +import { embeddablePluginMock } from '../../../../../mocks'; function DummySavedObjectFinder(props: { children: React.ReactNode }) { return ( @@ -43,10 +43,10 @@ function DummySavedObjectFinder(props: { children: React.ReactNode }) { } test('createNewEmbeddable() add embeddable to container', async () => { + const { setup, doStart } = embeddablePluginMock.createInstance(); const core = coreMock.createStart(); const { overlays } = core; const contactCardEmbeddableFactory = new ContactCardEmbeddableFactory( - {}, (() => null) as any, overlays ); @@ -55,7 +55,9 @@ test('createNewEmbeddable() add embeddable to container', async () => { firstName: 'foo', lastName: 'bar', } as any); - const getEmbeddableFactory = (id: string) => contactCardEmbeddableFactory; + setup.registerEmbeddableFactory(CONTACT_CARD_EMBEDDABLE, contactCardEmbeddableFactory); + const start = doStart(); + const getEmbeddableFactory = start.getEmbeddableFactory; const input: ContainerInput<{ firstName: string; lastName: string }> = { id: '1', panels: {}, @@ -66,8 +68,8 @@ test('createNewEmbeddable() add embeddable to container', async () => { new Set([contactCardEmbeddableFactory]).values()} + getFactory={getEmbeddableFactory} + getAllFactories={start.getEmbeddableFactories} notifications={core.notifications} SavedObjectFinder={() => null} /> @@ -88,10 +90,10 @@ test('createNewEmbeddable() add embeddable to container', async () => { }); test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()', async () => { + const { setup, doStart } = embeddablePluginMock.createInstance(); const core = coreMock.createStart(); const { overlays } = core; const contactCardEmbeddableFactory = new ContactCardEmbeddableFactory( - {}, (() => null) as any, overlays ); @@ -100,8 +102,10 @@ test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()' firstName: 'foo', lastName: 'bar', } as any); - const getEmbeddableFactory = ((id: string) => - contactCardEmbeddableFactory) as EmbeddableStart['getEmbeddableFactory']; + + setup.registerEmbeddableFactory(CONTACT_CARD_EMBEDDABLE, contactCardEmbeddableFactory); + const start = doStart(); + const getEmbeddableFactory = start.getEmbeddableFactory; const input: ContainerInput<{ firstName: string; lastName: string }> = { id: '1', panels: {}, @@ -113,7 +117,7 @@ test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()' container={container} onClose={onClose} getFactory={getEmbeddableFactory} - getAllFactories={() => new Set([contactCardEmbeddableFactory]).values()} + getAllFactories={start.getEmbeddableFactories} notifications={core.notifications} SavedObjectFinder={props => } /> diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx index 95eeb63710c32..06c47bd1bcad8 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -121,15 +121,16 @@ export class AddPanelFlyout extends React.Component { public render() { const SavedObjectFinder = this.props.SavedObjectFinder; + const metaData = [...this.props.getAllFactories()] + .filter( + embeddableFactory => + Boolean(embeddableFactory.savedObjectMetaData) && !embeddableFactory.isContainerType + ) + .map(({ savedObjectMetaData }) => savedObjectMetaData as any); const savedObjectsFinder = ( - Boolean(embeddableFactory.savedObjectMetaData) && !embeddableFactory.isContainerType - ) - .map(({ savedObjectMetaData }) => savedObjectMetaData as any)} + savedObjectMetaData={metaData} showFilter={true} noItemsMessage={i18n.translate('embeddableApi.addPanel.noMatchingObjectsMessage', { defaultMessage: 'No matching objects found.', diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts index 3f7c917cd1617..2f66d8eb0d619 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts @@ -32,18 +32,19 @@ import { ContactCardEmbeddableFactory, } from '../../../../test_samples/embeddables/contact_card/contact_card_embeddable_factory'; import { HelloWorldContainer } from '../../../../test_samples/embeddables/hello_world_container'; -import { EmbeddableFactory } from '../../../../embeddables'; +import { embeddablePluginMock } from '../../../../../mocks'; let container: Container; let embeddable: ContactCardEmbeddable; function createHelloWorldContainer(input = { id: '123', panels: {} }) { - const embeddableFactories = new Map(); - const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); - embeddableFactories.set( + const { setup, doStart } = embeddablePluginMock.createInstance(); + setup.registerEmbeddableFactory( CONTACT_CARD_EMBEDDABLE, - new ContactCardEmbeddableFactory({}, (() => {}) as any, {} as any) + new ContactCardEmbeddableFactory((() => {}) as any, {} as any) ); + const getEmbeddableFactory = doStart().getEmbeddableFactory; + return new HelloWorldContainer(input, { getEmbeddableFactory } as any); } diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx index e19acda8419da..ee31127cb5a40 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx @@ -28,20 +28,16 @@ import { } from '../../../test_samples'; // eslint-disable-next-line import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks'; -import { - EmbeddableFactory, - EmbeddableOutput, - isErrorEmbeddable, - ErrorEmbeddable, -} from '../../../embeddables'; +import { EmbeddableOutput, isErrorEmbeddable, ErrorEmbeddable } from '../../../embeddables'; import { of } from '../../../../tests/helpers'; import { esFilters } from '../../../../../../../plugins/data/public'; -import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; +import { embeddablePluginMock } from '../../../../mocks'; +import { EmbeddableStart } from '../../../../plugin'; -const setup = async () => { - const embeddableFactories = new Map(); - embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); - const getFactory = (id: string) => embeddableFactories.get(id); +const setupTests = async () => { + const { setup, doStart } = embeddablePluginMock.createInstance(); + setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); + const getFactory = doStart().getEmbeddableFactory; const container = new FilterableContainer( { id: 'hello', @@ -79,7 +75,7 @@ test('Is compatible when inspector adapters are available', async () => { const inspector = inspectorPluginMock.createStartContract(); inspector.isAvailable.mockImplementation(() => true); - const { embeddable } = await setup(); + const { embeddable } = await setupTests(); const inspectAction = new InspectPanelAction(inspector); expect(await inspectAction.isCompatible({ embeddable })).toBe(true); @@ -114,7 +110,7 @@ test('Executes when inspector adapters are available', async () => { const inspector = inspectorPluginMock.createStartContract(); inspector.isAvailable.mockImplementation(() => true); - const { embeddable } = await setup(); + const { embeddable } = await setupTests(); const inspectAction = new InspectPanelAction(inspector); expect(inspector.open).toHaveBeenCalledTimes(0); diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx index f4d5aa148373b..dea4a88bda082 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx @@ -19,7 +19,6 @@ import { EmbeddableOutput, isErrorEmbeddable } from '../../../'; import { RemovePanelAction } from './remove_panel_action'; -import { EmbeddableFactory } from '../../../embeddables'; import { EmbeddableStart } from '../../../../plugin'; import { FILTERABLE_EMBEDDABLE, @@ -31,11 +30,11 @@ import { FilterableContainer } from '../../../test_samples/embeddables/filterabl import { ViewMode } from '../../../types'; import { ContactCardEmbeddable } from '../../../test_samples/embeddables/contact_card/contact_card_embeddable'; import { esFilters, Filter } from '../../../../../../../plugins/data/public'; +import { embeddablePluginMock } from '../../../../mocks'; -const embeddableFactories = new Map(); -embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); -const getFactory = (id: string) => embeddableFactories.get(id); - +const { setup, doStart } = embeddablePluginMock.createInstance(); +setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); +const getFactory = doStart().getEmbeddableFactory; let container: FilterableContainer; let embeddable: FilterableEmbeddable; diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx index 20a5a8112f4d3..f977329562b9b 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx @@ -23,24 +23,21 @@ import { UiActionsStart } from 'src/plugins/ui_actions/public'; import { CoreStart } from 'src/core/public'; import { toMountPoint } from '../../../../../../kibana_react/public'; -import { EmbeddableFactory } from '../../../embeddables'; +import { EmbeddableFactoryDefinition } from '../../../embeddables'; import { Container } from '../../../containers'; import { ContactCardEmbeddable, ContactCardEmbeddableInput } from './contact_card_embeddable'; import { ContactCardInitializer } from './contact_card_initializer'; -import { EmbeddableFactoryOptions } from '../../../embeddables/embeddable_factory'; export const CONTACT_CARD_EMBEDDABLE = 'CONTACT_CARD_EMBEDDABLE'; -export class ContactCardEmbeddableFactory extends EmbeddableFactory { +export class ContactCardEmbeddableFactory + implements EmbeddableFactoryDefinition { public readonly type = CONTACT_CARD_EMBEDDABLE; constructor( - options: EmbeddableFactoryOptions, private readonly execTrigger: UiActionsStart['executeTriggerActions'], private readonly overlays: CoreStart['overlays'] - ) { - super(options); - } + ) {} public async isEditable() { return true; @@ -52,7 +49,7 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory> { + public getExplicitInput = (): Promise> => { return new Promise(resolve => { const modalSession = this.overlays.openModal( toMountPoint( @@ -72,9 +69,9 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory { return new ContactCardEmbeddable( initialInput, { @@ -82,5 +79,5 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory { +export class SlowContactCardEmbeddableFactory + implements EmbeddableFactoryDefinition { private loadTickCount = 0; public readonly type = CONTACT_CARD_EMBEDDABLE; constructor(private readonly options: SlowContactCardEmbeddableFactoryOptions) { - super(); if (options.loadTickCount) { this.loadTickCount = options.loadTickCount; } @@ -48,10 +46,10 @@ export class SlowContactCardEmbeddableFactory extends EmbeddableFactory< return 'slow to load contact card'; } - public async create(initialInput: ContactCardEmbeddableInput, parent?: Container) { + public create = async (initialInput: ContactCardEmbeddableInput, parent?: Container) => { for (let i = 0; i < this.loadTickCount; i++) { await Promise.resolve(); } return new ContactCardEmbeddable(initialInput, { execAction: this.options.execAction }, parent); - } + }; } diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container_factory.ts b/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container_factory.ts index 3488f6a2e038d..f27c7e8b011fd 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container_factory.ts +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container_factory.ts @@ -18,24 +18,21 @@ */ import { i18n } from '@kbn/i18n'; -import { Container, EmbeddableFactory } from '../..'; +import { Container, EmbeddableFactoryDefinition } from '../..'; import { FilterableContainer, FilterableContainerInput, FILTERABLE_CONTAINER, } from './filterable_container'; -import { EmbeddableFactoryOptions } from '../../embeddables/embeddable_factory'; import { EmbeddableStart } from '../../../plugin'; -export class FilterableContainerFactory extends EmbeddableFactory { +export class FilterableContainerFactory + implements EmbeddableFactoryDefinition { public readonly type = FILTERABLE_CONTAINER; constructor( - private readonly getFactory: EmbeddableStart['getEmbeddableFactory'], - options: EmbeddableFactoryOptions = {} - ) { - super(options); - } + private readonly getFactory: () => Promise + ) {} public getDisplayName() { return i18n.translate('embeddableApi.samples.filterableContainer.displayName', { @@ -47,7 +44,8 @@ export class FilterableContainerFactory extends EmbeddableFactory { + const getEmbeddableFactory = await this.getFactory(); + return new FilterableContainer(initialInput, getEmbeddableFactory, parent); + }; } diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_embeddable_factory.ts b/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_embeddable_factory.ts index f37a16ea86c43..4c941ee22abfa 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_embeddable_factory.ts @@ -23,10 +23,11 @@ import { FilterableEmbeddableInput, FILTERABLE_EMBEDDABLE, } from './filterable_embeddable'; -import { EmbeddableFactory } from '../../embeddables'; +import { EmbeddableFactoryDefinition } from '../../embeddables'; import { IContainer } from '../../containers'; -export class FilterableEmbeddableFactory extends EmbeddableFactory { +export class FilterableEmbeddableFactory + implements EmbeddableFactoryDefinition { public readonly type = FILTERABLE_EMBEDDABLE; public async isEditable() { diff --git a/src/plugins/embeddable/public/mocks.ts b/src/plugins/embeddable/public/mocks.ts index ba2f78e42e10e..2ee05d8316ace 100644 --- a/src/plugins/embeddable/public/mocks.ts +++ b/src/plugins/embeddable/public/mocks.ts @@ -30,6 +30,7 @@ export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { registerEmbeddableFactory: jest.fn(), + setCustomEmbeddableFactoryProvider: jest.fn(), }; return setupContract; }; diff --git a/src/plugins/embeddable/public/plugin.test.ts b/src/plugins/embeddable/public/plugin.test.ts index c334411004e2c..804f3e2e8a7b4 100644 --- a/src/plugins/embeddable/public/plugin.test.ts +++ b/src/plugins/embeddable/public/plugin.test.ts @@ -18,6 +18,9 @@ */ import { coreMock } from '../../../core/public/mocks'; import { testPlugin } from './tests/test_plugin'; +import { EmbeddableFactoryProvider } from './types'; +import { defaultEmbeddableFactoryProvider } from './lib'; +import { HelloWorldEmbeddable } from '../../../../examples/embeddable_examples/public'; test('cannot register embeddable factory with the same ID', async () => { const coreSetup = coreMock.createSetup(); @@ -33,3 +36,75 @@ test('cannot register embeddable factory with the same ID', async () => { 'Embeddable factory [embeddableFactoryId = ID] already registered in Embeddables API.' ); }); + +test('can set custom embeddable factory provider', async () => { + const coreSetup = coreMock.createSetup(); + const coreStart = coreMock.createStart(); + const { setup, doStart } = testPlugin(coreSetup, coreStart); + + const customProvider: EmbeddableFactoryProvider = def => ({ + ...defaultEmbeddableFactoryProvider(def), + getDisplayName: () => 'Intercepted!', + }); + + setup.setCustomEmbeddableFactoryProvider(customProvider); + setup.registerEmbeddableFactory('test', { + type: 'test', + create: () => Promise.resolve(undefined), + getDisplayName: () => 'Test', + isEditable: () => Promise.resolve(true), + }); + + const start = doStart(); + const factory = start.getEmbeddableFactory('test'); + expect(factory!.getDisplayName()).toEqual('Intercepted!'); +}); + +test('custom embeddable factory provider test for intercepting embeddable creation and destruction', async () => { + const coreSetup = coreMock.createSetup(); + const coreStart = coreMock.createStart(); + const { setup, doStart } = testPlugin(coreSetup, coreStart); + + let updateCount = 0; + const customProvider: EmbeddableFactoryProvider = def => { + return { + ...defaultEmbeddableFactoryProvider(def), + create: async (input, parent) => { + const embeddable = await defaultEmbeddableFactoryProvider(def).create(input, parent); + if (embeddable) { + const subscription = embeddable.getInput$().subscribe( + () => { + updateCount++; + }, + () => {}, + () => { + subscription.unsubscribe(); + updateCount = 0; + } + ); + } + return embeddable; + }, + }; + }; + + setup.setCustomEmbeddableFactoryProvider(customProvider); + setup.registerEmbeddableFactory('test', { + type: 'test', + create: (input, parent) => Promise.resolve(new HelloWorldEmbeddable(input, parent)), + getDisplayName: () => 'Test', + isEditable: () => Promise.resolve(true), + }); + + const start = doStart(); + const factory = start.getEmbeddableFactory('test'); + + const embeddable = await factory?.create({ id: '123' }); + embeddable!.updateInput({ title: 'boo' }); + // initial subscription, plus the second update. + expect(updateCount).toEqual(2); + + embeddable!.destroy(); + await new Promise(resolve => process.nextTick(resolve)); + expect(updateCount).toEqual(0); +}); diff --git a/src/plugins/embeddable/public/plugin.ts b/src/plugins/embeddable/public/plugin.ts index 381665c359ffd..a483f90f76dde 100644 --- a/src/plugins/embeddable/public/plugin.ts +++ b/src/plugins/embeddable/public/plugin.ts @@ -18,9 +18,16 @@ */ import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; -import { EmbeddableFactoryRegistry } from './types'; +import { EmbeddableFactoryRegistry, EmbeddableFactoryProvider } from './types'; import { bootstrap } from './bootstrap'; -import { EmbeddableFactory, EmbeddableInput, EmbeddableOutput } from './lib'; +import { + EmbeddableFactory, + EmbeddableInput, + EmbeddableOutput, + defaultEmbeddableFactoryProvider, + IEmbeddable, +} from './lib'; +import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; export interface EmbeddableSetupDependencies { uiActions: UiActionsSetup; @@ -29,21 +36,29 @@ export interface EmbeddableSetupDependencies { export interface EmbeddableSetup { registerEmbeddableFactory: ( id: string, - factory: EmbeddableFactory + factory: EmbeddableFactoryDefinition ) => void; + setCustomEmbeddableFactoryProvider: (customProvider: EmbeddableFactoryProvider) => void; } + export interface EmbeddableStart { getEmbeddableFactory: < I extends EmbeddableInput = EmbeddableInput, - O extends EmbeddableOutput = EmbeddableOutput + O extends EmbeddableOutput = EmbeddableOutput, + E extends IEmbeddable = IEmbeddable >( embeddableFactoryId: string - ) => EmbeddableFactory | undefined; + ) => EmbeddableFactory | undefined; getEmbeddableFactories: () => IterableIterator; } export class EmbeddablePublicPlugin implements Plugin { + private readonly embeddableFactoryDefinitions: Map< + string, + EmbeddableFactoryDefinition + > = new Map(); private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); + private customEmbeddableFactoryProvider?: EmbeddableFactoryProvider; constructor(initializerContext: PluginInitializerContext) {} @@ -52,34 +67,57 @@ export class EmbeddablePublicPlugin implements Plugin { + if (this.customEmbeddableFactoryProvider) { + throw new Error( + 'Custom embeddable factory provider is already set, and can only be set once' + ); + } + this.customEmbeddableFactoryProvider = provider; + }, }; } - public start(core: CoreStart) { + public start(core: CoreStart): EmbeddableStart { + this.embeddableFactoryDefinitions.forEach(def => { + this.embeddableFactories.set( + def.type, + this.customEmbeddableFactoryProvider + ? this.customEmbeddableFactoryProvider(def) + : defaultEmbeddableFactoryProvider(def) + ); + }); return { getEmbeddableFactory: this.getEmbeddableFactory, - getEmbeddableFactories: () => this.embeddableFactories.values(), + getEmbeddableFactories: () => { + this.ensureFactoriesExist(); + return this.embeddableFactories.values(); + }, }; } public stop() {} - private registerEmbeddableFactory = (embeddableFactoryId: string, factory: EmbeddableFactory) => { - if (this.embeddableFactories.has(embeddableFactoryId)) { + private registerEmbeddableFactory = ( + embeddableFactoryId: string, + factory: EmbeddableFactoryDefinition + ) => { + if (this.embeddableFactoryDefinitions.has(embeddableFactoryId)) { throw new Error( `Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] already registered in Embeddables API.` ); } - - this.embeddableFactories.set(embeddableFactoryId, factory); + this.embeddableFactoryDefinitions.set(embeddableFactoryId, factory); }; private getEmbeddableFactory = < I extends EmbeddableInput = EmbeddableInput, - O extends EmbeddableOutput = EmbeddableOutput + O extends EmbeddableOutput = EmbeddableOutput, + E extends IEmbeddable = IEmbeddable >( embeddableFactoryId: string - ) => { + ): EmbeddableFactory => { + this.ensureFactoryExists(embeddableFactoryId); const factory = this.embeddableFactories.get(embeddableFactoryId); if (!factory) { @@ -88,6 +126,24 @@ export class EmbeddablePublicPlugin implements Plugin; + return factory as EmbeddableFactory; }; + + // These two functions are only to support legacy plugins registering factories after the start lifecycle. + private ensureFactoriesExist() { + this.embeddableFactoryDefinitions.forEach(def => this.ensureFactoryExists(def.type)); + } + + private ensureFactoryExists(type: string) { + if (!this.embeddableFactories.get(type)) { + const def = this.embeddableFactoryDefinitions.get(type); + if (!def) return; + this.embeddableFactories.set( + type, + this.customEmbeddableFactoryProvider + ? this.customEmbeddableFactoryProvider(def) + : defaultEmbeddableFactoryProvider(def) + ); + } + } } diff --git a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts index 6beef35bbe136..54f3ac2887f6c 100644 --- a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts @@ -36,13 +36,13 @@ import { esFilters } from '../../../../plugins/data/public'; test('ApplyFilterAction applies the filter to the root of the container tree', async () => { const { doStart, setup } = testPlugin(); - const api = doStart(); - const factory1 = new FilterableContainerFactory(api.getEmbeddableFactory); const factory2 = new FilterableEmbeddableFactory(); - - setup.registerEmbeddableFactory(factory1.type, factory1); + const factory1 = new FilterableContainerFactory(async () => await api.getEmbeddableFactory); setup.registerEmbeddableFactory(factory2.type, factory2); + setup.registerEmbeddableFactory(factory1.type, factory1); + + const api = doStart(); const applyFilterAction = createFilterAction(); @@ -63,7 +63,9 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a FilterableContainer >(FILTERABLE_CONTAINER, { panels: {}, id: 'Node2' }); - if (isErrorEmbeddable(node1) || isErrorEmbeddable(node2)) throw new Error(); + if (isErrorEmbeddable(node1) || isErrorEmbeddable(node2)) { + throw new Error(); + } const embeddable = await node2.addNewEmbeddable< FilterableEmbeddableInput, @@ -94,9 +96,11 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a test('ApplyFilterAction is incompatible if the root container does not accept a filter as input', async () => { const { doStart, coreStart, setup } = testPlugin(); - const api = doStart(); const inspector = inspectorPluginMock.createStartContract(); + const factory = new FilterableEmbeddableFactory(); + setup.registerEmbeddableFactory(factory.type, factory); + const api = doStart(); const applyFilterAction = createFilterAction(); const parent = new HelloWorldContainer( { id: 'root', panels: {} }, @@ -110,10 +114,6 @@ test('ApplyFilterAction is incompatible if the root container does not accept a SavedObjectFinder: () => null, } ); - - const factory = new FilterableEmbeddableFactory(); - setup.registerEmbeddableFactory(factory.type, factory); - const embeddable = await parent.addNewEmbeddable< FilterableContainerInput, EmbeddableOutput, @@ -130,12 +130,12 @@ test('ApplyFilterAction is incompatible if the root container does not accept a test('trying to execute on incompatible context throws an error ', async () => { const { doStart, coreStart, setup } = testPlugin(); - const api = doStart(); const inspector = inspectorPluginMock.createStartContract(); const factory = new FilterableEmbeddableFactory(); setup.registerEmbeddableFactory(factory.type, factory); + const api = doStart(); const applyFilterAction = createFilterAction(); const parent = new HelloWorldContainer( { id: 'root', panels: {} }, diff --git a/src/plugins/embeddable/public/tests/container.test.ts b/src/plugins/embeddable/public/tests/container.test.ts index 1ee52f4749135..87076399465d3 100644 --- a/src/plugins/embeddable/public/tests/container.test.ts +++ b/src/plugins/embeddable/public/tests/container.test.ts @@ -56,8 +56,6 @@ async function creatHelloWorldContainerAndEmbeddable( const coreSetup = coreMock.createSetup(); const coreStart = coreMock.createStart(); const { setup, doStart, uiActions } = testPlugin(coreSetup, coreStart); - const start = doStart(); - const filterableFactory = new FilterableEmbeddableFactory(); const slowContactCardFactory = new SlowContactCardEmbeddableFactory({ execAction: uiActions.executeTriggerActions, @@ -68,6 +66,8 @@ async function creatHelloWorldContainerAndEmbeddable( setup.registerEmbeddableFactory(slowContactCardFactory.type, slowContactCardFactory); setup.registerEmbeddableFactory(helloWorldFactory.type, helloWorldFactory); + const start = doStart(); + const container = new HelloWorldContainer(containerInput, { getActions: uiActions.getTriggerCompatibleActions, getEmbeddableFactory: start.getEmbeddableFactory, @@ -563,6 +563,13 @@ test('Container changes made directly after adding a new embeddable are propagat const coreSetup = coreMock.createSetup(); const coreStart = coreMock.createStart(); const { setup, doStart, uiActions } = testPlugin(coreSetup, coreStart); + + const factory = new SlowContactCardEmbeddableFactory({ + loadTickCount: 3, + execAction: uiActions.executeTriggerActions, + }); + setup.registerEmbeddableFactory(factory.type, factory); + const start = doStart(); const container = new HelloWorldContainer( @@ -582,12 +589,6 @@ test('Container changes made directly after adding a new embeddable are propagat } ); - const factory = new SlowContactCardEmbeddableFactory({ - loadTickCount: 3, - execAction: uiActions.executeTriggerActions, - }); - setup.registerEmbeddableFactory(factory.type, factory); - const subscription = Rx.merge(container.getOutput$(), container.getInput$()) .pipe(skip(2)) .subscribe(() => { @@ -759,12 +760,13 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem coreMock.createSetup(), coreMock.createStart() ); - const start = doStart(); const factory = new SlowContactCardEmbeddableFactory({ loadTickCount: 3, execAction: uiActions.executeTriggerActions, }); setup.registerEmbeddableFactory(factory.type, factory); + + const start = doStart(); const container = new HelloWorldContainer( { id: 'hello', @@ -799,12 +801,12 @@ test('adding a panel then subsequently removing it before its loaded removes the coreMock.createSetup(), coreMock.createStart() ); - const start = doStart(); const factory = new SlowContactCardEmbeddableFactory({ loadTickCount: 1, execAction: uiActions.executeTriggerActions, }); setup.registerEmbeddableFactory(factory.type, factory); + const start = doStart(); const container = new HelloWorldContainer( { id: 'hello', diff --git a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx index 99d5a7c747d15..19e461b8bde7e 100644 --- a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx +++ b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx @@ -47,15 +47,14 @@ beforeEach(async () => { coreMock.createSetup(), coreMock.createStart() ); - api = doStart(); const contactCardFactory = new ContactCardEmbeddableFactory( - {}, uiActions.executeTriggerActions, {} as any ); setup.registerEmbeddableFactory(contactCardFactory.type, contactCardFactory); + api = doStart(); container = new HelloWorldContainer( { id: '123', panels: {} }, { diff --git a/src/plugins/embeddable/public/tests/explicit_input.test.ts b/src/plugins/embeddable/public/tests/explicit_input.test.ts index f0a7219531b59..0e03db3ec8358 100644 --- a/src/plugins/embeddable/public/tests/explicit_input.test.ts +++ b/src/plugins/embeddable/public/tests/explicit_input.test.ts @@ -41,7 +41,6 @@ const { setup, doStart, coreStart, uiActions } = testPlugin( coreMock.createSetup(), coreMock.createStart() ); -const start = doStart(); setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); const factory = new SlowContactCardEmbeddableFactory({ @@ -51,6 +50,8 @@ const factory = new SlowContactCardEmbeddableFactory({ setup.registerEmbeddableFactory(CONTACT_CARD_EMBEDDABLE, factory); setup.registerEmbeddableFactory(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory()); +const start = doStart(); + test('Explicit embeddable input mapped to undefined will default to inherited', async () => { const derivedFilter: Filter = { $state: { store: esFilters.FilterStateStore.APP_STATE }, diff --git a/src/plugins/embeddable/public/tests/get_embeddable_factories.test.ts b/src/plugins/embeddable/public/tests/get_embeddable_factories.test.ts index 1a222cd548de7..1989d6356cbd1 100644 --- a/src/plugins/embeddable/public/tests/get_embeddable_factories.test.ts +++ b/src/plugins/embeddable/public/tests/get_embeddable_factories.test.ts @@ -35,16 +35,16 @@ test('returns empty list if there are no embeddable factories', () => { test('returns existing embeddable factories', () => { const { setup, doStart } = testPlugin(); - const start = doStart(); - const { length } = [...start.getEmbeddableFactories()]; - const factory1 = new FilterableContainerFactory(start.getEmbeddableFactory); - const factory2 = new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any); + const factory1 = new FilterableContainerFactory(async () => await start.getEmbeddableFactory); + const factory2 = new ContactCardEmbeddableFactory((() => null) as any, {} as any); setup.registerEmbeddableFactory(factory1.type, factory1); setup.registerEmbeddableFactory(factory2.type, factory2); + const start = doStart(); + const list = [...start.getEmbeddableFactories()]; - expect(list.length - length).toBe(2); + expect(list.length).toBe(2); expect(!!list.find(({ type }) => factory1.type === type)).toBe(true); expect(!!list.find(({ type }) => factory2.type === type)).toBe(true); }); diff --git a/src/plugins/embeddable/public/types.ts b/src/plugins/embeddable/public/types.ts index 7a879c389f3c4..2d112b2359818 100644 --- a/src/plugins/embeddable/public/types.ts +++ b/src/plugins/embeddable/public/types.ts @@ -17,6 +17,22 @@ * under the License. */ -import { EmbeddableFactory } from './lib/embeddables'; +import { SavedObjectAttributes } from 'kibana/public'; +import { + EmbeddableFactory, + EmbeddableInput, + EmbeddableOutput, + IEmbeddable, + EmbeddableFactoryDefinition, +} from './lib/embeddables'; export type EmbeddableFactoryRegistry = Map; + +export type EmbeddableFactoryProvider = < + I extends EmbeddableInput = EmbeddableInput, + O extends EmbeddableOutput = EmbeddableOutput, + E extends IEmbeddable = IEmbeddable, + T extends SavedObjectAttributes = SavedObjectAttributes +>( + def: EmbeddableFactoryDefinition +) => EmbeddableFactory; diff --git a/src/plugins/es_ui_shared/public/request/np_ready_request.ts b/src/plugins/es_ui_shared/public/request/np_ready_request.ts index 6771abd64df7e..06af698f2ce02 100644 --- a/src/plugins/es_ui_shared/public/request/np_ready_request.ts +++ b/src/plugins/es_ui_shared/public/request/np_ready_request.ts @@ -43,7 +43,7 @@ export interface UseRequestResponse { isInitialRequest: boolean; isLoading: boolean; error: E | null; - data: D | null; + data?: D | null; sendRequest: (...args: any[]) => Promise>; } diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts index 5626fc80bb749..dc8321aa07004 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts @@ -17,25 +17,6 @@ * under the License. */ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - import { ValidationFunc } from '../../hook_form_lib'; import { isJSON } from '../../../validators/string'; import { ERROR_CODE } from './types'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts index a8d24984cec7c..0509b8081c35b 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts @@ -28,9 +28,9 @@ interface Props { } export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => { - const [formData, setFormData] = useState({}); - const previousRawData = useRef({}); const form = useFormContext(); + const previousRawData = useRef(form.__formData$.current.value); + const [formData, setFormData] = useState(previousRawData.current); useEffect(() => { const subscription = form.subscribe(({ data: { raw } }) => { @@ -41,6 +41,7 @@ export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) = const valuesToWatchArray = Array.isArray(pathsToWatch) ? (pathsToWatch as string[]) : ([pathsToWatch] as string[]); + if (valuesToWatchArray.some(value => previousRawData.current[value] !== raw[value])) { previousRawData.current = raw; setFormData(raw); diff --git a/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap b/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap index 57cbe0f17498f..c1dc560b4353f 100644 --- a/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap +++ b/src/plugins/home/public/application/components/__snapshots__/add_data.test.js.snap @@ -104,6 +104,7 @@ exports[`apmUiEnabled 1`] = ` { footer={ diff --git a/src/plugins/home/public/application/components/feature_directory.js b/src/plugins/home/public/application/components/feature_directory.js index 2e979bf589975..7d827b1ca9229 100644 --- a/src/plugins/home/public/application/components/feature_directory.js +++ b/src/plugins/home/public/application/components/feature_directory.js @@ -89,6 +89,7 @@ export class FeatureDirectory extends React.Component { renderTabs = () => { return this.tabs.map((tab, index) => ( this.onSelectedTabChanged(tab.id)} isSelected={tab.id === this.state.selectedTabId} key={index} diff --git a/src/plugins/home/public/application/components/home.js b/src/plugins/home/public/application/components/home.js index 77cde6a574aec..5263dc06e96fc 100644 --- a/src/plugins/home/public/application/components/home.js +++ b/src/plugins/home/public/application/components/home.js @@ -203,7 +203,7 @@ export class Home extends Component {

- + & { id: string }, + parent?: IContainer +): Promise => { + const savedVisualizations = getSavedVisualizationsLoader(); + + try { + const visId = vis.id as string; + + const editUrl = visId + ? getHttp().basePath.prepend(`/app/kibana${savedVisualizations.urlFor(visId)}`) + : ''; + const isLabsEnabled = getUISettings().get('visualize:enableLabs'); + + if (!isLabsEnabled && vis.type.stage === 'experimental') { + return new DisabledLabEmbeddable(vis.title, input); + } + + const indexPattern = vis.data.indexPattern; + const indexPatterns = indexPattern ? [indexPattern] : []; + const editable = getCapabilities().visualize.save as boolean; + return new VisualizeEmbeddable( + getTimeFilter(), + { + vis, + indexPatterns, + editUrl, + editable, + }, + input, + parent + ); + } catch (e) { + console.error(e); // eslint-disable-line no-console + return new ErrorEmbeddable(e, input, parent); + } +}; diff --git a/src/plugins/visualizations/public/embeddable/index.ts b/src/plugins/visualizations/public/embeddable/index.ts index 78f9827ffde3e..3753c4dbbb9ed 100644 --- a/src/plugins/visualizations/public/embeddable/index.ts +++ b/src/plugins/visualizations/public/embeddable/index.ts @@ -21,3 +21,4 @@ export { VisualizeEmbeddable, VisualizeInput } from './visualize_embeddable'; export { VisualizeEmbeddableFactory } from './visualize_embeddable_factory'; export { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; export { VIS_EVENT_TO_TRIGGER } from './events'; +export { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object'; diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 0c7e732f0b185..e64d200251797 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -33,8 +33,8 @@ import { EmbeddableInput, EmbeddableOutput, Embeddable, - Container, EmbeddableVisTriggerContext, + IContainer, } from '../../../../plugins/embeddable/public'; import { dispatchRenderComplete } from '../../../../plugins/kibana_utils/public'; import { IExpressionLoaderParams, ExpressionsStart } from '../../../../plugins/expressions/public'; @@ -89,7 +89,7 @@ export class VisualizeEmbeddable extends Embeddable { +export class VisualizeEmbeddableFactory + implements + EmbeddableFactoryDefinition< + VisualizeInput, + VisualizeOutput | EmbeddableOutput, + VisualizeEmbeddable | DisabledLabEmbeddable, + VisualizationAttributes + > { public readonly type = VISUALIZE_EMBEDDABLE_TYPE; - - constructor() { - super({ - savedObjectMetaData: { - name: i18n.translate('visualizations.savedObjectName', { defaultMessage: 'Visualization' }), - includeFields: ['visState'], - type: 'visualization', - getIconForSavedObject: savedObject => { - return ( - getTypes().get(JSON.parse(savedObject.attributes.visState).type).icon || 'visualizeApp' - ); - }, - getTooltipForSavedObject: savedObject => { - return `${savedObject.attributes.title} (${ - getTypes().get(JSON.parse(savedObject.attributes.visState).type).title - })`; - }, - showSavedObject: savedObject => { - const typeName: string = JSON.parse(savedObject.attributes.visState).type; - const visType = getTypes().get(typeName); - if (!visType) { - return false; - } - if (getUISettings().get('visualize:enableLabs')) { - return true; - } - return visType.stage !== 'experimental'; - }, - }, - }); - } + public readonly savedObjectMetaData: SavedObjectMetaData = { + name: i18n.translate('visualizations.savedObjectName', { defaultMessage: 'Visualization' }), + includeFields: ['visState'], + type: 'visualization', + getIconForSavedObject: savedObject => { + return ( + getTypes().get(JSON.parse(savedObject.attributes.visState).type).icon || 'visualizeApp' + ); + }, + getTooltipForSavedObject: savedObject => { + return `${savedObject.attributes.title} (${ + getTypes().get(JSON.parse(savedObject.attributes.visState).type).title + })`; + }, + showSavedObject: savedObject => { + const typeName: string = JSON.parse(savedObject.attributes.visState).type; + const visType = getTypes().get(typeName); + if (!visType) { + return false; + } + if (getUISettings().get('visualize:enableLabs')) { + return true; + } + return visType.stage !== 'experimental'; + }, + }; + constructor() {} public async isEditable() { return getCapabilities().visualize.save as boolean; @@ -93,56 +91,17 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< }); } - public async createFromObject( - vis: Vis, - input: Partial & { id: string }, - parent?: Container - ): Promise { - const savedVisualizations = getSavedVisualizationsLoader(); - - try { - const visId = vis.id as string; - - const editUrl = visId - ? getHttp().basePath.prepend(`/app/kibana${savedVisualizations.urlFor(visId)}`) - : ''; - const isLabsEnabled = getUISettings().get('visualize:enableLabs'); - - if (!isLabsEnabled && vis.type.stage === 'experimental') { - return new DisabledLabEmbeddable(vis.title, input); - } - - const indexPattern = vis.data.indexPattern; - const indexPatterns = indexPattern ? [indexPattern] : []; - const editable = await this.isEditable(); - return new VisualizeEmbeddable( - getTimeFilter(), - { - vis, - indexPatterns, - editUrl, - editable, - }, - input, - parent - ); - } catch (e) { - console.error(e); // eslint-disable-line no-console - return new ErrorEmbeddable(e, input, parent); - } - } - public async createFromSavedObject( savedObjectId: string, input: Partial & { id: string }, - parent?: Container + parent?: IContainer ): Promise { const savedVisualizations = getSavedVisualizationsLoader(); try { const savedObject = await savedVisualizations.get(savedObjectId); const vis = new Vis(savedObject.visState.type, await convertToSerializedVis(savedObject)); - return this.createFromObject(vis, input, parent); + return createVisEmbeddableFromObject(vis, input, parent); } catch (e) { console.error(e); // eslint-disable-line no-console return new ErrorEmbeddable(e, input, parent); diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index f4983a4313c4d..2aa346423297a 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -43,6 +43,9 @@ const createStartContract = (): VisualizationsStart => ({ createVis: jest.fn(), convertFromSerializedVis: jest.fn(), convertToSerializedVis: jest.fn(), + __LEGACY: { + createVisEmbeddableFromObject: jest.fn(), + }, }); const createInstance = async () => { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index d3e7b759a4416..216defcee9016 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -37,7 +37,11 @@ import { setChrome, setOverlays, } from './services'; -import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeEmbeddableFactory } from './embeddable'; +import { + VISUALIZE_EMBEDDABLE_TYPE, + VisualizeEmbeddableFactory, + createVisEmbeddableFromObject, +} from './embeddable'; import { ExpressionsSetup, ExpressionsStart } from '../../../plugins/expressions/public'; import { EmbeddableSetup } from '../../../plugins/embeddable/public'; import { visualization as visualizationFunction } from './expressions/visualization_function'; @@ -69,6 +73,7 @@ export interface VisualizationsStart extends TypesStart { convertToSerializedVis: typeof convertToSerializedVis; convertFromSerializedVis: typeof convertFromSerializedVis; showNewVisModal: typeof showNewVisModal; + __LEGACY: { createVisEmbeddableFromObject: typeof createVisEmbeddableFromObject }; } export interface VisualizationsSetupDeps { @@ -163,6 +168,7 @@ export class VisualizationsPlugin convertToSerializedVis, convertFromSerializedVis, savedVisualizationsLoader, + __LEGACY: { createVisEmbeddableFromObject }, }; } diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 24e97aa081e51..4e106ef3e039a 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -54,7 +54,11 @@ module.exports = function(grunt) { return [ 'http://localhost:5610/test_bundle/built_css.css', - `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.distFilename}`, + ...UiSharedDeps.jsDepFilenames.map( + chunkFilename => `http://localhost:5610/bundles/kbn-ui-shared-deps/${chunkFilename}` + ), + `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, + 'http://localhost:5610/built_assets/dlls/vendors_runtime.bundle.dll.js', ...DllCompiler.getRawDllConfig().chunks.map( chunk => `http://localhost:5610/built_assets/dlls/vendors${chunk}.bundle.dll.js` diff --git a/test/accessibility/apps/management.ts b/test/accessibility/apps/management.ts index ac2921ed063f5..9e75250403d6b 100644 --- a/test/accessibility/apps/management.ts +++ b/test/accessibility/apps/management.ts @@ -35,7 +35,8 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { // await PageObjects.common.navigateToApp('settings'); // }); - describe('Management', () => { + // FLAKY: https://github.com/elastic/kibana/issues/60470 + describe.skip('Management', () => { before(async () => { await esArchiver.load('discover'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/functional/apps/context/_context_navigation.js b/test/functional/apps/context/_context_navigation.js new file mode 100644 index 0000000000000..e4066d4159b9e --- /dev/null +++ b/test/functional/apps/context/_context_navigation.js @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; + +const TEST_COLUMN_NAMES = ['@message']; +const TEST_FILTER_COLUMN_NAMES = [ + ['extension', 'jpg'], + ['geo.src', 'IN'], +]; + +export default function({ getService, getPageObjects }) { + const browser = getService('browser'); + const docTable = getService('docTable'); + const PageObjects = getPageObjects(['common', 'context', 'discover', 'timePicker']); + + describe('context link in discover', function contextSize() { + before(async function() { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await Promise.all( + TEST_COLUMN_NAMES.map(columnName => PageObjects.discover.clickFieldListItemAdd(columnName)) + ); + for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { + await PageObjects.discover.clickFieldListItem(columnName); + await PageObjects.discover.clickFieldListPlusFilter(columnName, value); + } + }); + + it('should go back after loading', async function() { + // navigate to the context view + await docTable.clickRowToggle({ rowIndex: 0 }); + await (await docTable.getRowActions({ rowIndex: 0 }))[0].click(); + await PageObjects.context.waitUntilContextLoadingHasFinished(); + await PageObjects.context.clickSuccessorLoadMoreButton(); + await PageObjects.context.clickSuccessorLoadMoreButton(); + await PageObjects.context.clickSuccessorLoadMoreButton(); + await PageObjects.context.waitUntilContextLoadingHasFinished(); + await browser.goBack(); + await PageObjects.discover.waitForDocTableLoadingComplete(); + const hitCount = await PageObjects.discover.getHitCount(); + expect(hitCount).to.be('1,556'); + }); + }); +} diff --git a/test/functional/apps/context/index.js b/test/functional/apps/context/index.js index c3c938c623731..ffd7daf42c642 100644 --- a/test/functional/apps/context/index.js +++ b/test/functional/apps/context/index.js @@ -38,6 +38,7 @@ export default function({ getService, getPageObjects, loadTestFile }) { return esArchiver.unload('logstash_functional'); }); + loadTestFile(require.resolve('./_context_navigation')); loadTestFile(require.resolve('./_discover_navigation')); loadTestFile(require.resolve('./_filters')); loadTestFile(require.resolve('./_size')); diff --git a/test/functional/apps/dashboard/bwc_shared_urls.js b/test/functional/apps/dashboard/bwc_shared_urls.js index fb1e580135e5a..b56cb658b80bb 100644 --- a/test/functional/apps/dashboard/bwc_shared_urls.js +++ b/test/functional/apps/dashboard/bwc_shared_urls.js @@ -135,6 +135,27 @@ export default function({ getService, getPageObjects }) { await dashboardExpect.selectedLegendColorCount('#000000', 5); }); + + it('back button works for old dashboards after state migrations', async () => { + await PageObjects.dashboard.preserveCrossAppState(); + const oldId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.selectedLegendColorCount('#000000', 5); + + const url = `${kibanaBaseUrl}#/dashboard?${urlQuery}`; + log.debug(`Navigating to ${url}`); + await browser.get(url); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.selectedLegendColorCount('#F9D9F9', 5); + await browser.goBack(); + + await PageObjects.header.waitUntilLoadingHasFinished(); + const newId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); + expect(newId).to.be.equal(oldId); + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.selectedLegendColorCount('#000000', 5); + }); }); }); } diff --git a/test/functional/apps/dashboard/dashboard_time.js b/test/functional/apps/dashboard/dashboard_time.js index 2e7b7f9a6dbb1..5a2628f42ded5 100644 --- a/test/functional/apps/dashboard/dashboard_time.js +++ b/test/functional/apps/dashboard/dashboard_time.js @@ -91,6 +91,20 @@ export default function({ getPageObjects, getService }) { expect(time.start).to.equal('~ an hour ago'); expect(time.end).to.equal('now'); }); + + it('should use saved time, if time is missing in global state, but _g is present in the url', async function() { + const currentUrl = await browser.getCurrentUrl(); + const kibanaBaseUrl = currentUrl.substring(0, currentUrl.indexOf('#')); + const id = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); + + await PageObjects.dashboard.gotoDashboardLandingPage(); + + const urlWithGlobalTime = `${kibanaBaseUrl}#/dashboard/${id}?_g=(filters:!())`; + await browser.get(urlWithGlobalTime, false); + const time = await PageObjects.timePicker.getTimeConfig(); + expect(time.start).to.equal(PageObjects.timePicker.defaultStartTime); + expect(time.end).to.equal(PageObjects.timePicker.defaultEndTime); + }); }); // If the user has time stored with a dashboard, it's supposed to override the current time settings diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js index f0a7844b29987..08e0cb0b8d23a 100644 --- a/test/functional/apps/discover/_doc_navigation.js +++ b/test/functional/apps/discover/_doc_navigation.js @@ -31,7 +31,8 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); const esArchiver = getService('esArchiver'); - describe('doc link in discover', function contextSize() { + // FLAKY: https://github.com/elastic/kibana/issues/62281 + describe.skip('doc link in discover', function contextSize() { this.tags('smoke'); before(async function() { await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/functional/apps/discover/_field_visualize.ts b/test/functional/apps/discover/_field_visualize.ts index 24f4ba592324c..f8f290b259b7e 100644 --- a/test/functional/apps/discover/_field_visualize.ts +++ b/test/functional/apps/discover/_field_visualize.ts @@ -32,8 +32,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }; - // FLAKY: https://github.com/elastic/kibana/issues/61714 - describe.skip('discover field visualize button', () => { + describe('discover field visualize button', () => { before(async function() { log.debug('load kibana index with default index pattern'); await esArchiver.load('discover'); @@ -50,7 +49,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should visualize a field in area chart', async () => { - await PageObjects.discover.clickFieldListItem('phpmemory'); + await PageObjects.discover.findFieldByName('phpmemory'); log.debug('visualize a phpmemory field'); await PageObjects.discover.clickFieldListItemVisualize('phpmemory'); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -83,7 +82,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { it('should preserve app filters in visualize', async () => { await filterBar.addFilter('bytes', 'is between', '3500', '4000'); - await PageObjects.discover.clickFieldListItem('geo.src'); + await PageObjects.discover.findFieldByName('geo.src'); log.debug('visualize a geo.src field with filter applied'); await PageObjects.discover.clickFieldListItemVisualize('geo.src'); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -119,7 +118,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { it('should preserve query in visualize', async () => { await queryBar.setQuery('machine.os : ios'); await queryBar.submitQuery(); - await PageObjects.discover.clickFieldListItem('geo.dest'); + await PageObjects.discover.findFieldByName('geo.dest'); log.debug('visualize a geo.dest field with query applied'); await PageObjects.discover.clickFieldListItemVisualize('geo.dest'); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/test/functional/apps/management/_kibana_settings.js b/test/functional/apps/management/_kibana_settings.js index c99368ba4e859..97337d4573e2a 100644 --- a/test/functional/apps/management/_kibana_settings.js +++ b/test/functional/apps/management/_kibana_settings.js @@ -46,6 +46,18 @@ export default function({ getService, getPageObjects }) { }); describe('state:storeInSessionStorage', () => { + async function getStateFromUrl() { + const currentUrl = await browser.getCurrentUrl(); + let match = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/); + if (match) return [match[2], match[3]]; + match = currentUrl.match(/(.*)?_a=(.*)&_g=(.*)/); + if (match) return [match[3], match[2]]; + + if (!match) { + throw new Error('State in url is missing or malformed'); + } + } + it('defaults to null', async () => { await PageObjects.settings.clickKibanaSettings(); const storeInSessionStorage = await PageObjects.settings.getAdvancedSettingCheckbox( @@ -58,10 +70,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - const currentUrl = await browser.getCurrentUrl(); - const urlPieces = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/); - const globalState = urlPieces[2]; - const appState = urlPieces[3]; + const [globalState, appState] = await getStateFromUrl(); // We don't have to be exact, just need to ensure it's greater than when the hashed variation is being used, // which is less than 20 characters. @@ -83,10 +92,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - const currentUrl = await browser.getCurrentUrl(); - const urlPieces = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/); - const globalState = urlPieces[2]; - const appState = urlPieces[3]; + const [globalState, appState] = await getStateFromUrl(); // We don't have to be exact, just need to ensure it's less than the unhashed version, which will be // greater than 20 characters with the default state plus a time. @@ -100,10 +106,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.settings.clickKibanaSettings(); await PageObjects.settings.toggleAdvancedSettingCheckbox('state:storeInSessionStorage'); await PageObjects.header.clickDashboard(); - const currentUrl = await browser.getCurrentUrl(); - const urlPieces = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/); - const globalState = urlPieces[2]; - const appState = urlPieces[3]; + const [globalState, appState] = await getStateFromUrl(); // We don't have to be exact, just need to ensure it's greater than when the hashed variation is being used, // which is less than 20 characters. expect(globalState.length).to.be.greaterThan(20); diff --git a/test/functional/apps/visualize/_vertical_bar_chart.js b/test/functional/apps/visualize/_vertical_bar_chart.js index c07a1bf6e812f..04969a07eef6f 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart.js +++ b/test/functional/apps/visualize/_vertical_bar_chart.js @@ -76,6 +76,25 @@ export default function({ getService, getPageObjects }) { }); }); + describe('bar charts range on x axis', () => { + it('should individual bars for each configured range', async function() { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVerticalBarChart(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await PageObjects.visEditor.clickBucket('X-axis'); + log.debug('Aggregation = Date Range'); + await PageObjects.visEditor.selectAggregation('Date Range'); + log.debug('Field = @timestamp'); + await PageObjects.visEditor.selectField('@timestamp'); + await PageObjects.visEditor.clickAddDateRange(); + await PageObjects.visEditor.setDateRangeByIndex('1', 'now-2w/w', 'now-1w/w'); + await PageObjects.visEditor.clickGo(); + const bottomLabels = await PageObjects.visChart.getXAxisLabels(); + expect(bottomLabels.length).to.be(2); + }); + }); + // FLAKY: https://github.com/elastic/kibana/issues/22322 describe.skip('vertical bar chart flaky part', function() { const vizName1 = 'Visualization VerticalBarChart'; diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 10652ce3ec4b2..2377c32a80b5b 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -40,6 +40,11 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider return await el.getVisibleText(); } + public async findFieldByName(name: string) { + const fieldSearch = await testSubjects.find('fieldFilterSearchInput'); + await fieldSearch.type(name); + } + public async saveSearch(searchName: string) { log.debug('saveSearch'); await this.clickSaveSearchButton(); @@ -239,10 +244,16 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider await testSubjects.click(`fieldToggle-${field}`); } - public async clickFieldListItemVisualize(field: string) { - return await retry.try(async () => { - await testSubjects.click(`fieldVisualize-${field}`); - }); + public async clickFieldListItemVisualize(fieldName: string) { + const field = await testSubjects.find(`field-${fieldName}-showDetails`); + const isActive = await field.elementHasClass('dscSidebarItem--active'); + + if (!isActive) { + // expand the field to show the "Visualize" button + await field.click(); + } + + await testSubjects.click(`fieldVisualize-${fieldName}`); } public async expectFieldListItemVisualize(field: string) { diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index 6225b4e3aca62..6fdc306e39192 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -79,6 +79,39 @@ export function HomePageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.click(`launchSampleDataSet${id}`); } + async clickAllKibanaPlugins() { + await testSubjects.click('allPlugins'); + } + + async clickVisualizeExplorePlugins() { + await testSubjects.click('tab-data'); + } + + async clickAdminPlugin() { + await testSubjects.click('tab-admin'); + } + + async clickOnConsole() { + await testSubjects.click('homeSynopsisLinkconsole'); + } + async clickOnLogo() { + await testSubjects.click('logo'); + } + + async ClickOnLogsData() { + await testSubjects.click('logsData'); + } + + // clicks on Active MQ logs + async clickOnLogsTutorial() { + await testSubjects.click('homeSynopsisLinkactivemq logs'); + } + + // clicks on cloud tutorial link + async clickOnCloudTutorial() { + await testSubjects.click('onCloudTutorial'); + } + async loadSavedObjects() { await retry.try(async () => { await testSubjects.click('loadSavedObjects'); diff --git a/test/functional/page_objects/visualize_editor_page.ts b/test/functional/page_objects/visualize_editor_page.ts index b1c3e924b3c1b..41c12170cf4dc 100644 --- a/test/functional/page_objects/visualize_editor_page.ts +++ b/test/functional/page_objects/visualize_editor_page.ts @@ -103,6 +103,15 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP await radioBtn.click(); } + public async clickAddDateRange() { + await testSubjects.click(`visEditorAddDateRange`); + } + + public async setDateRangeByIndex(index: string, from: string, to: string) { + await testSubjects.setValue(`visEditorDateRange${index}__from`, from); + await testSubjects.setValue(`visEditorDateRange${index}__to`, to); + } + /** * Adds new bucket * @param bucketName bucket name, like 'X-axis', 'Split rows', 'Split series' diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx index f8625e4490e51..fd07416cadbc5 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx @@ -29,7 +29,6 @@ import { import { DASHBOARD_CONTAINER_TYPE, DashboardContainer, - DashboardContainerFactory, DashboardContainerInput, } from '../../../../../../../../src/plugins/dashboard/public'; @@ -70,8 +69,9 @@ export class DashboardContainerExample extends React.Component { this.mounted = true; const dashboardFactory = this.props.getEmbeddableFactory< DashboardContainerInput, - ContainerOutput - >(DASHBOARD_CONTAINER_TYPE) as DashboardContainerFactory; + ContainerOutput, + DashboardContainer + >(DASHBOARD_CONTAINER_TYPE); if (dashboardFactory) { this.container = await dashboardFactory.create(dashboardInput); if (this.mounted) { diff --git a/vars/workers.groovy b/vars/workers.groovy index c5638f2624fe5..1c55c676d9425 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -1,23 +1,38 @@ // "Workers" in this file will spin up an instance, do some setup etc depending on the configuration, and then execute some work that you define // e.g. workers.base(name: 'my-worker') { sh "echo 'ready to execute some kibana scripts'" } +def label(size) { + switch(size) { + case 's': + return 'linux && immutable' + case 'l': + return 'tests-l' + case 'xl': + return 'tests-xl' + case 'xxl': + return 'tests-xxl' + } + + error "unknown size '${size}'" +} + /* The base worker that all of the others use. Will clone the scm (assumed to be kibana), and run kibana bootstrap processes by default. Parameters: - label - gobld/agent label to use, e.g. 'linux && immutable' + size - size of worker label to use, e.g. 's' or 'xl' ramDisk - Should the workspace be mounted in memory? Default: true bootstrapped - If true, download kibana dependencies, run kbn bootstrap, etc. Default: true name - Name of the worker for display purposes, filenames, etc. scm - Jenkins scm configuration for checking out code. Use `null` to disable checkout. Default: inherited from job */ def base(Map params, Closure closure) { - def config = [label: '', ramDisk: true, bootstrapped: true, name: 'unnamed-worker', scm: scm] + params - if (!config.label) { - error "You must specify an agent label, such as 'tests-xl' or 'linux && immutable', when using workers.base()" + def config = [size: '', ramDisk: true, bootstrapped: true, name: 'unnamed-worker', scm: scm] + params + if (!config.size) { + error "You must specify an agent size, such as 'xl' or 's', when using workers.base()" } - node(config.label) { + node(label(config.size)) { agentInfo.print() if (config.ramDisk) { @@ -88,7 +103,7 @@ def ci(Map params, Closure closure) { // Worker for running the current intake jobs. Just runs a single script after bootstrap. def intake(jobName, String script) { return { - ci(name: jobName, label: 'linux && immutable', ramDisk: false) { + ci(name: jobName, size: 's', ramDisk: false) { withEnv(["JOB=${jobName}"]) { runbld(script, "Execute ${jobName}") } @@ -99,7 +114,7 @@ def intake(jobName, String script) { // Worker for running functional tests. Runs a setup process (e.g. the kibana build) then executes a map of closures in parallel (e.g. one for each ciGroup) def functional(name, Closure setup, Map processes) { return { - parallelProcesses(name: name, setup: setup, processes: processes, delayBetweenProcesses: 20, label: 'tests-xl') + parallelProcesses(name: name, setup: setup, processes: processes, delayBetweenProcesses: 20, size: 'xl') } } @@ -111,12 +126,12 @@ def functional(name, Closure setup, Map processes) { setup: Closure to execute after the agent is bootstrapped, before starting the parallel work processes: Map of closures that will execute in parallel after setup. Each closure is passed a unique number. delayBetweenProcesses: Number of seconds to wait between starting the parallel processes. Useful to spread the load of heavy init processes, e.g. Elasticsearch starting up. Default: 0 - label: gobld/agent label to use, e.g. 'linux && immutable'. Default: 'tests-xl', a 32 CPU machine used for running many functional test suites in parallel + size: size of worker label to use, e.g. 's' or 'xl' */ def parallelProcesses(Map params) { - def config = [name: 'parallel-worker', setup: {}, processes: [:], delayBetweenProcesses: 0, label: 'tests-xl'] + params + def config = [name: 'parallel-worker', setup: {}, processes: [:], delayBetweenProcesses: 0, size: 'xl'] + params - ci(label: config.label, name: config.name) { + ci(size: config.size, name: config.name) { config.setup() def nextProcessNumber = 1 diff --git a/x-pack/legacy/plugins/apm/e2e/package.json b/x-pack/legacy/plugins/apm/e2e/package.json index 57500dfe3fdc8..150ad5bb13d0b 100644 --- a/x-pack/legacy/plugins/apm/e2e/package.json +++ b/x-pack/legacy/plugins/apm/e2e/package.json @@ -13,6 +13,7 @@ "@types/cypress-cucumber-preprocessor": "^1.14.1", "@types/js-yaml": "^3.12.1", "@types/node": "^10.12.11", + "axios": "^0.19.2", "cypress": "^4.2.0", "cypress-cucumber-preprocessor": "^2.0.1", "js-yaml": "^3.13.1", @@ -21,6 +22,7 @@ "p-retry": "^4.2.0", "ts-loader": "^6.2.2", "typescript": "3.8.3", + "yargs": "^15.3.1", "wait-on": "^4.0.1", "webpack": "^4.42.1" } diff --git a/x-pack/legacy/plugins/apm/e2e/run-e2e.sh b/x-pack/legacy/plugins/apm/e2e/run-e2e.sh index 5e55dc1eb834d..7c17c14dc9601 100755 --- a/x-pack/legacy/plugins/apm/e2e/run-e2e.sh +++ b/x-pack/legacy/plugins/apm/e2e/run-e2e.sh @@ -75,6 +75,13 @@ if [ $? -ne 0 ]; then exit 1 fi +# +# Cypress +################################################## +echo "\n${bold}Cypress (logs: ${TMP_DIR}/e2e-yarn.log)${normal}" +echo "Installing cypress dependencies " +yarn &> ${TMP_DIR}/e2e-yarn.log + # # Static mock data ################################################## @@ -99,13 +106,6 @@ if [ $? -ne 0 ]; then exit 1 fi -# -# Cypress -################################################## -echo "\n${bold}Cypress (logs: ${TMP_DIR}/e2e-yarn.log)${normal}" -echo "Installing cypress dependencies " -yarn &> ${TMP_DIR}/e2e-yarn.log - # # Wait for Kibana to start ################################################## diff --git a/x-pack/legacy/plugins/apm/e2e/yarn.lock b/x-pack/legacy/plugins/apm/e2e/yarn.lock index b7b531a9c73c0..c023c64eb1cf4 100644 --- a/x-pack/legacy/plugins/apm/e2e/yarn.lock +++ b/x-pack/legacy/plugins/apm/e2e/yarn.lock @@ -1288,7 +1288,7 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== @@ -1429,6 +1429,13 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== +axios@^0.19.2: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + babel-loader@^8.0.2: version "8.0.6" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.6.tgz#e33bdb6f362b03f4bb141a0c21ab87c501b70dfb" @@ -1845,6 +1852,11 @@ cachedir@2.3.0: resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + caniuse-lite@^1.0.30001023: version "1.0.30001027" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001027.tgz#283e2ef17d94889cc216a22c6f85303d78ca852d" @@ -2010,6 +2022,15 @@ cli-truncate@^0.2.1: slice-ansi "0.0.4" string-width "^1.0.1" +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -2417,7 +2438,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" -debug@3.1.0: +debug@3.1.0, debug@=3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== @@ -2438,6 +2459,11 @@ debug@^3.0.1, debug@^3.1.0: dependencies: ms "^2.1.1" +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -2621,6 +2647,11 @@ elliptic@^6.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" @@ -2933,6 +2964,14 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + flush-write-stream@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" @@ -2951,6 +2990,13 @@ folktale@2.3.2: resolved "https://registry.yarnpkg.com/folktale/-/folktale-2.3.2.tgz#38231b039e5ef36989920cbf805bf6b227bf4fd4" integrity sha512-+8GbtQBwEqutP0v3uajDDoN64K2ehmHd0cjlghhxh0WpcfPzAIjPA03e1VvHlxL02FVGR0A6lwXsNQKn3H1RNQ== +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -3041,6 +3087,11 @@ get-assigned-identifiers@^1.2.0: resolved "https://registry.yarnpkg.com/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz#6dbf411de648cbaf8d9169ebb0d2d576191e2ff1" integrity sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ== +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + get-func-name@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" @@ -3418,6 +3469,11 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-generator@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/is-generator/-/is-generator-1.0.3.tgz#c14c21057ed36e328db80347966c693f886389f3" @@ -3779,6 +3835,13 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + lodash.clonedeep@4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -4328,7 +4391,7 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= -p-limit@^2.0.0, p-limit@^2.2.1: +p-limit@^2.0.0, p-limit@^2.2.0, p-limit@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== @@ -4342,6 +4405,13 @@ p-locate@^3.0.0: dependencies: p-limit "^2.0.0" +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + p-map@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" @@ -4428,6 +4498,11 @@ path-exists@^3.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -4836,11 +4911,21 @@ request@cypress-io/request#b5af0d1fa47eec97ba980cde90a13e69a2afcd16: tunnel-agent "^0.6.0" uuid "^3.3.2" +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + require-from-string@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -4982,6 +5067,11 @@ serialize-javascript@^2.1.2: resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -5316,6 +5406,15 @@ string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + string_decoder@^1.0.0, string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -5856,6 +5955,11 @@ webpack@^4.42.1: watchpack "^1.6.0" webpack-sources "^1.4.1" +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -5878,6 +5982,15 @@ wrap-ansi@^3.0.1: string-width "^2.1.1" strip-ansi "^4.0.0" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -5903,6 +6016,31 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yargs-parser@^18.1.1: + version "18.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.2.tgz#2f482bea2136dbde0861683abea7756d30b504f1" + integrity sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@^15.3.1: + version "15.3.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b" + integrity sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.1" + yauzl@2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts index 949264fcc9fdb..b0083eb4f87e2 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts @@ -57,7 +57,7 @@ export const WorkpadExport = compose( ({ workpad, pageCount, kibana }: Props & WithKibanaProps): ComponentProps => ({ getExportUrl: type => { if (type === 'pdf') { - const pdfUrl = getPdfUrl(workpad, { pageCount }, kibana.services.http.basePath.prepend); + const pdfUrl = getPdfUrl(workpad, { pageCount }, kibana.services.http.basePath); return getAbsoluteUrl(pdfUrl); } @@ -78,7 +78,7 @@ export const WorkpadExport = compose( onExport: type => { switch (type) { case 'pdf': - return createPdf(workpad, { pageCount }, kibana.services.http.basePath.prepend) + return createPdf(workpad, { pageCount }, kibana.services.http.basePath) .then(({ data }: { data: { job: { id: string } } }) => { notify.info(strings.getExportPDFMessage(), { title: strings.getExportPDFTitle(workpad.name), diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.test.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.test.ts index ceaf82c1c07d6..6c7d7ddd0a793 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.test.ts +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.test.ts @@ -9,29 +9,34 @@ jest.mock('../../../../common/lib/fetch'); import { getPdfUrl, createPdf } from './utils'; import { workpads } from '../../../../__tests__/fixtures/workpads'; import { fetch } from '../../../../common/lib/fetch'; +import { IBasePath } from 'kibana/public'; -const addBasePath = jest.fn().mockImplementation(s => `basepath/${s}`); +const basePath = ({ + prepend: jest.fn().mockImplementation(s => `basepath/s/spacey/${s}`), + get: () => 'basepath/s/spacey', + serverBasePath: `basepath`, +} as unknown) as IBasePath; const workpad = workpads[0]; test('getPdfUrl returns the correct url', () => { - const url = getPdfUrl(workpad, { pageCount: 2 }, addBasePath); + const url = getPdfUrl(workpad, { pageCount: 2 }, basePath); expect(url).toMatchInlineSnapshot( - `"basepath//api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FPhoenix,layout:(dimensions:(height:0,width:0),id:preserve_layout),objectType:'canvas%20workpad',relativeUrls:!(%2Fapp%2Fcanvas%23%2Fexport%2Fworkpad%2Fpdf%2Fbase-workpad%2Fpage%2F1,%2Fapp%2Fcanvas%23%2Fexport%2Fworkpad%2Fpdf%2Fbase-workpad%2Fpage%2F2),title:'base%20workpad')"` + `"basepath/s/spacey//api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FPhoenix,layout:(dimensions:(height:0,width:0),id:preserve_layout),objectType:'canvas%20workpad',relativeUrls:!(%2Fs%2Fspacey%2Fapp%2Fcanvas%23%2Fexport%2Fworkpad%2Fpdf%2Fbase-workpad%2Fpage%2F1,%2Fs%2Fspacey%2Fapp%2Fcanvas%23%2Fexport%2Fworkpad%2Fpdf%2Fbase-workpad%2Fpage%2F2),title:'base%20workpad')"` ); }); test('createPdf posts to create the pdf', () => { - createPdf(workpad, { pageCount: 2 }, addBasePath); + createPdf(workpad, { pageCount: 2 }, basePath); expect(fetch.post).toBeCalled(); const args = (fetch.post as jest.MockedFunction).mock.calls[0]; - expect(args[0]).toMatchInlineSnapshot(`"basepath//api/reporting/generate/printablePdf"`); + expect(args[0]).toMatchInlineSnapshot(`"basepath/s/spacey//api/reporting/generate/printablePdf"`); expect(args[1]).toMatchInlineSnapshot(` Object { - "jobParams": "(browserTimezone:America/Phoenix,layout:(dimensions:(height:0,width:0),id:preserve_layout),objectType:'canvas workpad',relativeUrls:!(/app/canvas#/export/workpad/pdf/base-workpad/page/1,/app/canvas#/export/workpad/pdf/base-workpad/page/2),title:'base workpad')", + "jobParams": "(browserTimezone:America/Phoenix,layout:(dimensions:(height:0,width:0),id:preserve_layout),objectType:'canvas workpad',relativeUrls:!(/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/1,/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/2),title:'base workpad')", } `); }); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts index 5adbf4ce66c13..dc99c0687f388 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts @@ -6,6 +6,7 @@ import rison from 'rison-node'; // @ts-ignore Untyped local. +import { IBasePath } from 'kibana/public'; import { fetch } from '../../../../common/lib/fetch'; import { CanvasWorkpad } from '../../../../types'; import { url } from '../../../../../../../../src/plugins/kibana_utils/public'; @@ -17,9 +18,7 @@ interface PageCount { pageCount: number; } -type AddBasePath = (url: string) => string; - -type Arguments = [CanvasWorkpad, PageCount, AddBasePath]; +type Arguments = [CanvasWorkpad, PageCount, IBasePath]; interface PdfUrlData { createPdfUri: string; @@ -29,10 +28,11 @@ interface PdfUrlData { function getPdfUrlParts( { id, name: title, width, height }: CanvasWorkpad, { pageCount }: PageCount, - addBasePath: (path: string) => string + basePath: IBasePath ): PdfUrlData { - const reportingEntry = addBasePath('/api/reporting/generate'); - const canvasEntry = '/app/canvas#'; + const reportingEntry = basePath.prepend('/api/reporting/generate'); + const urlPrefix = basePath.get().replace(basePath.serverBasePath, ''); // for Spaces prefix, which is included in basePath.get() + const canvasEntry = `${urlPrefix}/app/canvas#`; // The viewport in Reporting by specifying the dimensions. In order for things to work, // we need a viewport that will include all of the pages in the workpad. The viewport diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index 1caea1b4b728f..99a59c756e228 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -18,7 +18,7 @@ import { } from '../../../../../../../src/plugins/data/public'; import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; import { - EmbeddableFactory as AbstractEmbeddableFactory, + EmbeddableFactoryDefinition, ErrorEmbeddable, EmbeddableInput, IContainer, @@ -36,25 +36,22 @@ interface StartServices { indexPatternService: IndexPatternsContract; } -export class EmbeddableFactory extends AbstractEmbeddableFactory { +export class EmbeddableFactory implements EmbeddableFactoryDefinition { type = DOC_TYPE; + savedObjectMetaData = { + name: i18n.translate('xpack.lens.lensSavedObjectLabel', { + defaultMessage: 'Lens Visualization', + }), + type: DOC_TYPE, + getIconForSavedObject: () => 'lensApp', + }; - constructor(private getStartServices: () => Promise) { - super({ - savedObjectMetaData: { - name: i18n.translate('xpack.lens.lensSavedObjectLabel', { - defaultMessage: 'Lens Visualization', - }), - type: DOC_TYPE, - getIconForSavedObject: () => 'lensApp', - }, - }); - } + constructor(private getStartServices: () => Promise) {} - public async isEditable() { + public isEditable = async () => { const { capabilities } = await this.getStartServices(); return capabilities.visualize.save as boolean; - } + }; canCreateNew() { return false; @@ -66,11 +63,11 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { }); } - async createFromSavedObject( + createFromSavedObject = async ( savedObjectId: string, input: Partial & { id: string }, parent?: IContainer - ) { + ) => { const { savedObjectsClient, coreHttp, @@ -111,7 +108,7 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { input, parent ); - } + }; async create(input: EmbeddableInput) { return new ErrorEmbeddable('Lens can only be created from a saved object', input); diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.js index 415630d9f730b..aa55cf0808ef2 100644 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.js +++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.js @@ -174,9 +174,16 @@ export function removeTrackedLayerStateForSelectedLayer() { export function replaceLayerList(newLayerList) { return (dispatch, getState) => { - getLayerListRaw(getState()).forEach(({ id }) => { - dispatch(removeLayerFromLayerList(id)); - }); + const isMapReady = getMapReady(getState()); + if (!isMapReady) { + dispatch({ + type: CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST, + }); + } else { + getLayerListRaw(getState()).forEach(({ id }) => { + dispatch(removeLayerFromLayerList(id)); + }); + } newLayerList.forEach(layerDescriptor => { dispatch(addLayer(layerDescriptor)); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js index fdc8ad2176d08..2995ea039e7a8 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js @@ -235,12 +235,12 @@ export class MBMapContainer extends React.Component { //clamping ot -89/89 latitudes since Mapboxgl does not seem to handle bounds that contain the poles (logs errors to the console when using -90/90) const lnLatBounds = new mapboxgl.LngLatBounds( new mapboxgl.LngLat( - clampToLonBounds(goto.bounds.min_lon), - clampToLatBounds(goto.bounds.min_lat) + clampToLonBounds(goto.bounds.minLon), + clampToLatBounds(goto.bounds.minLat) ), new mapboxgl.LngLat( - clampToLonBounds(goto.bounds.max_lon), - clampToLatBounds(goto.bounds.max_lat) + clampToLonBounds(goto.bounds.maxLon), + clampToLatBounds(goto.bounds.maxLat) ) ); //maxZoom ensure we're not zooming in too far on single points or small shapes diff --git a/x-pack/legacy/plugins/maps/public/embeddable/README.md b/x-pack/legacy/plugins/maps/public/embeddable/README.md index 1de327702fb87..8ce3794e2ed2c 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/README.md +++ b/x-pack/legacy/plugins/maps/public/embeddable/README.md @@ -30,17 +30,15 @@ ### Creating a Map embeddable from state ``` const factory = new MapEmbeddableFactory(); -const state = { - layerList: [], // where layerList is same as saved object layerListJSON property (unstringified) - title: 'my map', -} const input = { hideFilterActions: true, isLayerTOCOpen: false, openTOCDetails: ['tfi3f', 'edh66'], mapCenter: { lat: 0.0, lon: 0.0, zoom: 7 } } -const mapEmbeddable = await factory.createFromState(state, input, parent); +const mapEmbeddable = await factory.create(input, parent); +// where layerList is same as saved object layerListJSON property (unstringified)) +mapEmbeddable.setLayerList([]); ``` #### Customize tooltip @@ -62,7 +60,9 @@ const renderTooltipContent = ({ addFilters, closeTooltip, features, isLocked, lo return
Custom tooltip content
; } -const mapEmbeddable = await factory.createFromState(state, input, parent, renderTooltipContent); +const mapEmbeddable = await factory.create(input, parent) +mapEmbeddable.setLayerList(layerList); +mapEmbeddable.setRenderTooltipContent(renderTooltipContent); ``` @@ -80,7 +80,10 @@ const eventHandlers = { }, } -const mapEmbeddable = await factory.createFromState(state, input, parent, renderTooltipContent, eventHandlers); +const mapEmbeddable = await factory.create(input, parent); +mapEmbeddable.setLayerList(layerList); +mapEmbeddable.setRenderTooltipContent(renderTooltipContent); +mapEmbeddable.setEventHandlers(eventHandlers); ``` @@ -90,55 +93,13 @@ Geojson sources will not update unless you modify `__featureCollection` property ``` const factory = new MapEmbeddableFactory(); -const state = { - layerList: [ - { - 'id': 'gaxya', - 'label': 'My geospatial data', - 'minZoom': 0, - 'maxZoom': 24, - 'alpha': 1, - 'sourceDescriptor': { - 'id': 'b7486', - 'type': 'GEOJSON_FILE', - '__featureCollection': { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [0, 0], [10, 10], [10, 0], [0, 0] - ] - ] - }, - "properties": { - "name": "null island", - "another_prop": "something else interesting" - } - } - ] - } - }, - 'visible': true, - 'style': { - 'type': 'VECTOR', - 'properties': {} - }, - 'type': 'VECTOR' - } - ], - title: 'my map', -} const input = { hideFilterActions: true, isLayerTOCOpen: false, openTOCDetails: ['tfi3f', 'edh66'], mapCenter: { lat: 0.0, lon: 0.0, zoom: 7 } } -const mapEmbeddable = await factory.createFromState(state, input, parent); +const mapEmbeddable = await factory.create(input, parent); mapEmbeddable.setLayerList([ { diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx index 3c9069c7a836f..9544e8714f265 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx @@ -129,6 +129,14 @@ export class MapEmbeddable extends Embeddable this.onContainerStateChanged(input)); } + setRenderTooltipContent = (renderTooltipContent: RenderToolTipContent) => { + this._renderTooltipContent = renderTooltipContent; + }; + + setEventHandlers = (eventHandlers: EventHandlers) => { + this._eventHandlers = eventHandlers; + }; + getInspectorAdapters() { return getInspectorAdapters(this._store.getState()); } diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts index b9cb66f831281..5a036ed47fb62 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -14,8 +14,7 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { MapEmbeddable, MapEmbeddableInput } from './map_embeddable'; import { getIndexPatternService } from '../kibana_services'; import { - EmbeddableFactory, - ErrorEmbeddable, + EmbeddableFactoryDefinition, IContainer, } from '../../../../../../src/plugins/embeddable/public'; @@ -28,25 +27,17 @@ import { getInitialLayers } from '../angular/get_initial_layers'; import { mergeInputWithSavedMap } from './merge_input_with_saved_map'; import '../angular/services/gis_map_saved_object_loader'; import { bindSetupCoreAndPlugins, bindStartCoreAndPlugins } from '../plugin'; -import { RenderToolTipContent } from '../layers/tooltips/tooltip_property'; -import { - EventHandlers, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; -export class MapEmbeddableFactory extends EmbeddableFactory { +export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { type = MAP_SAVED_OBJECT_TYPE; - + savedObjectMetaData = { + name: i18n.translate('xpack.maps.mapSavedObjectLabel', { + defaultMessage: 'Map', + }), + type: MAP_SAVED_OBJECT_TYPE, + getIconForSavedObject: () => APP_ICON, + }; constructor() { - super({ - savedObjectMetaData: { - name: i18n.translate('xpack.maps.mapSavedObjectLabel', { - defaultMessage: 'Map', - }), - type: MAP_SAVED_OBJECT_TYPE, - getIconForSavedObject: () => APP_ICON, - }, - }); // Init required services. Necessary while in legacy bindSetupCoreAndPlugins(npSetup.core, npSetup.plugins); bindStartCoreAndPlugins(npStart.core, npStart.plugins); @@ -103,11 +94,11 @@ export class MapEmbeddableFactory extends EmbeddableFactory { return await savedObjectLoader.get(savedObjectId); } - async createFromSavedObject( + createFromSavedObject = async ( savedObjectId: string, input: MapEmbeddableInput, parent?: IContainer - ) { + ) => { const savedMap = await this._fetchSavedMap(savedObjectId); const layerList = getInitialLayers(savedMap.layerListJSON); const indexPatterns = await this._getIndexPatterns(layerList); @@ -135,39 +126,23 @@ export class MapEmbeddableFactory extends EmbeddableFactory { } return embeddable; - } + }; - async createFromState( - state: { title?: string; layerList?: unknown[] }, - input: MapEmbeddableInput, - parent: IContainer, - renderTooltipContent: RenderToolTipContent, - eventHandlers: EventHandlers - ) { - const layerList = state && state.layerList ? state.layerList : getInitialLayers(); + create = async (input: MapEmbeddableInput, parent?: IContainer) => { + const layerList = getInitialLayers(); const indexPatterns = await this._getIndexPatterns(layerList); return new MapEmbeddable( { layerList, - title: state && state.title ? state.title : '', + title: input.title ?? '', indexPatterns, editable: false, }, input, - parent, - renderTooltipContent, - eventHandlers - ); - } - - async create(input: MapEmbeddableInput) { - window.location.href = chrome.addBasePath(createMapPath('')); - return new ErrorEmbeddable( - 'Maps can only be created with createFromSavedObject or createFromState', - input + parent ); - } + }; } npSetup.plugins.embeddable.registerEmbeddableFactory( diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.d.ts b/x-pack/legacy/plugins/maps/public/layers/layer.d.ts index 777566298e607..de59642ede8ab 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/layer.d.ts @@ -3,12 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { LayerDescriptor } from '../../common/descriptor_types'; +import { LayerDescriptor, MapExtent, MapFilters } from '../../common/descriptor_types'; import { ISource } from './sources/source'; import { DataRequest } from './util/data_request'; import { SyncContext } from '../actions/map_actions'; export interface ILayer { + getBounds(mapFilters: MapFilters): Promise; getDataRequest(id: string): DataRequest | undefined; getDisplayName(source?: ISource): Promise; getId(): string; @@ -25,6 +26,7 @@ export interface ILayerArguments { export class AbstractLayer implements ILayer { constructor(layerArguments: ILayerArguments); + getBounds(mapFilters: MapFilters): Promise; getDataRequest(id: string): DataRequest | undefined; getDisplayName(source?: ISource): Promise; getId(): string; diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index d162e342dfd1a..e9616be89b601 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -320,12 +320,12 @@ export class AbstractLayer { return sourceDataRequest && sourceDataRequest.hasData(); } - async getBounds() { + async getBounds(/* mapFilters: MapFilters */) { return { - min_lon: -180, - max_lon: 180, - min_lat: -89, - max_lat: 89, + minLon: -180, + maxLon: 180, + minLat: -89, + maxLat: 89, }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index bf04a73cfba77..441d52d23398a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -175,10 +175,10 @@ export class AbstractESSource extends AbstractVectorSource { } return { - min_lon: esBounds.top_left.lon, - max_lon: esBounds.bottom_right.lon, - min_lat: esBounds.bottom_right.lat, - max_lat: esBounds.top_left.lat, + minLon: esBounds.top_left.lon, + maxLon: esBounds.bottom_right.lon, + minLat: esBounds.bottom_right.lat, + maxLat: esBounds.top_left.lat, }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts index 7a747da244233..1400654297e01 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts +++ b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.d.ts @@ -8,7 +8,11 @@ import { FeatureCollection } from 'geojson'; import { AbstractSource, ISource } from './source'; import { IField } from '../fields/field'; -import { ESSearchSourceResponseMeta } from '../../../common/descriptor_types'; +import { + ESSearchSourceResponseMeta, + MapExtent, + VectorSourceRequestMeta, +} from '../../../common/descriptor_types'; export type GeoJsonFetchMeta = ESSearchSourceResponseMeta; @@ -18,6 +22,7 @@ export type GeoJsonWithMeta = { }; export interface IVectorSource extends ISource { + getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent; getGeoJsonWithMeta( layerName: 'string', searchFilters: unknown[], @@ -29,6 +34,7 @@ export interface IVectorSource extends ISource { } export class AbstractVectorSource extends AbstractSource implements IVectorSource { + getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent; getGeoJsonWithMeta( layerName: 'string', searchFilters: unknown[], diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js index bf57306df5697..eadaf42ca694d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js @@ -99,6 +99,7 @@ export class ColorMapSelect extends Component { ); } else @@ -108,6 +109,7 @@ export class ColorMapSelect extends Component { field={this.props.styleProperty.getField()} getValueSuggestions={this.props.styleProperty.getValueSuggestions} onChange={this._onCustomColorMapChange} + swatches={this.props.swatches} /> ); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js index 059543d705fc7..20fd97a229352 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js @@ -8,61 +8,8 @@ import _ from 'lodash'; import React from 'react'; import { removeRow, isColorInvalid } from './color_stops_utils'; import { i18n } from '@kbn/i18n'; -import { EuiButtonIcon, EuiColorPicker, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; - -function getColorStopRow({ index, errors, stopInput, onColorChange, color, deleteButton, onAdd }) { - const colorPickerButtons = ( -
- {deleteButton} - -
- ); - return ( - - - - {stopInput} - - - - - - - ); -} - -export function getDeleteButton(onRemove) { - return ( - - ); -} +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { MbValidatedColorPicker } from './mb_validated_color_picker'; export const ColorStops = ({ onChange, @@ -72,6 +19,7 @@ export const ColorStops = ({ renderStopInput, addNewRow, canDeleteStop, + swatches, }) => { function getStopInput(stop, index) { const onStopChange = newStopValue => { @@ -134,10 +82,56 @@ export const ColorStops = ({ isInvalid: isStopsInvalid(newColorStops), }); }; - deleteButton = getDeleteButton(onRemove); + deleteButton = ( + + ); } - return getColorStopRow({ index, errors, stopInput, onColorChange, color, deleteButton, onAdd }); + const colorPickerButtons = ( +
+ {deleteButton} + +
+ ); + return ( + + + + {stopInput} + + + + + + + ); }); return
{rows}
; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js index edf230b0a945c..0656173e5c411 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js @@ -27,6 +27,7 @@ export const ColorStopsCategorical = ({ field, onChange, getValueSuggestions, + swatches, }) => { const getStopError = (stop, index) => { let count = 0; @@ -81,6 +82,7 @@ export const ColorStopsCategorical = ({ renderStopInput={renderStopInput} canDeleteStop={canDeleteStop} addNewRow={addCategoricalRow} + swatches={swatches} /> ); }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js index 0f6a0583d3dbc..4e2d07b9dfea0 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js @@ -20,6 +20,7 @@ import { i18n } from '@kbn/i18n'; export const ColorStopsOrdinal = ({ colorStops = [{ stop: 0, color: DEFAULT_CUSTOM_COLOR }], onChange, + swatches, }) => { const getStopError = (stop, index) => { let error; @@ -69,6 +70,7 @@ export const ColorStopsOrdinal = ({ renderStopInput={renderStopInput} canDeleteStop={canDeleteStop} addNewRow={addOrdinalRow} + swatches={swatches} /> ); }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js index 5e8f720fcc5e3..460e7379920c4 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js @@ -18,6 +18,7 @@ export function DynamicColorForm({ onDynamicStyleChange, staticDynamicSelect, styleProperty, + swatches, }) { const styleOptions = styleProperty.getOptions(); @@ -101,6 +102,7 @@ export function DynamicColorForm({ useCustomColorMap={_.get(styleOptions, 'useCustomColorRamp', false)} styleProperty={styleProperty} showColorMapTypeToggle={showColorMapTypeToggle} + swatches={swatches} /> ); } else if (styleProperty.isCategorical()) { @@ -118,6 +120,7 @@ export function DynamicColorForm({ useCustomColorMap={_.get(styleOptions, 'useCustomColorPalette', false)} styleProperty={styleProperty} showColorMapTypeToggle={showColorMapTypeToggle} + swatches={swatches} /> ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/mb_validated_color_picker.tsx b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/mb_validated_color_picker.tsx new file mode 100644 index 0000000000000..b4fad6690b9ac --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/mb_validated_color_picker.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component } from 'react'; +import { isValidHex, EuiColorPicker, EuiFormControlLayoutProps } from '@elastic/eui'; + +export const RGBA_0000 = 'rgba(0,0,0,0)'; + +interface Props { + onChange: (color: string) => void; + color: string; + swatches?: string[]; + append?: EuiFormControlLayoutProps['append']; +} + +interface State { + colorInputValue: string; +} + +// EuiColorPicker treats '' or invalid colors as transparent. +// Mapbox logs errors for '' or invalid colors. +// MbValidatedColorPicker is a wrapper around EuiColorPicker that reconciles the behavior difference +// between the two by returning a Mapbox safe RGBA_0000 for '' or invalid colors +// while keeping invalid state local so EuiColorPicker's input properly handles text input. +export class MbValidatedColorPicker extends Component { + state = { + colorInputValue: this.props.color === RGBA_0000 ? '' : this.props.color, + }; + + _onColorChange = (color: string) => { + // reflect all user input, whether valid or not + this.setState({ colorInputValue: color }); + // Only surface mapbox valid input to caller + this.props.onChange(isValidHex(color) ? color : RGBA_0000); + }; + + render() { + return ( + + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_form.js index ab1634a53a966..a295556ee3126 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_form.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_form.js @@ -5,7 +5,8 @@ */ import React from 'react'; -import { EuiColorPicker, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { MbValidatedColorPicker } from './mb_validated_color_picker'; export function StaticColorForm({ onStaticStyleChange, @@ -23,11 +24,10 @@ export function StaticColorForm({ {staticDynamicSelect} - diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js index 146bc40aa8531..e671f00b78381 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js @@ -18,11 +18,10 @@ import { EuiTextColor, } from '@elastic/eui'; import { Category } from '../components/legend/category'; -import { COLOR_MAP_TYPE } from '../../../../../common/constants'; +import { COLOR_MAP_TYPE, RGBA_0000 } from '../../../../../common/constants'; import { isCategoricalStopsInvalid } from '../components/color/color_stops_utils'; const EMPTY_STOPS = { stops: [], defaultColor: null }; -const RGBA_0000 = 'rgba(0,0,0,0)'; export class DynamicColorProperty extends DynamicStyleProperty { syncCircleColorWithMb(mbLayerId, mbMap, alpha) { diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 6b89554546330..d606420909281 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -167,10 +167,10 @@ export class VectorLayer extends AbstractLayer { features: visibleFeatures, }); return { - min_lon: bbox[0], - min_lat: bbox[1], - max_lon: bbox[2], - max_lat: bbox[3], + minLon: bbox[0], + minLat: bbox[1], + maxLon: bbox[2], + maxLat: bbox[3], }; } diff --git a/x-pack/legacy/plugins/reporting/common/constants.ts b/x-pack/legacy/plugins/reporting/common/constants.ts index 1746345879192..8f7a06ba9f8e9 100644 --- a/x-pack/legacy/plugins/reporting/common/constants.ts +++ b/x-pack/legacy/plugins/reporting/common/constants.ts @@ -27,6 +27,9 @@ export const WHITELISTED_JOB_CONTENT_TYPES = [ 'image/png', ]; +// See: +// https://github.com/chromium/chromium/blob/3611052c055897e5ebbc5b73ea295092e0c20141/services/network/public/cpp/header_util_unittest.cc#L50 +// For a list of headers that chromium doesn't like export const KBN_SCREENSHOT_HEADER_BLACKLIST = [ 'accept-encoding', 'connection', @@ -38,8 +41,14 @@ export const KBN_SCREENSHOT_HEADER_BLACKLIST = [ // only for a single transport-level connection, and shouldn't // be stored by caches or forwarded by proxies. 'transfer-encoding', + 'trailer', + 'te', + 'upgrade', + 'keep-alive', ]; +export const KBN_SCREENSHOT_HEADER_BLACKLIST_STARTS_WITH_PATTERN = ['proxy-']; + export const UI_SETTINGS_CUSTOM_PDF_LOGO = 'xpackReporting:customPdfLogo'; /** diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.test.ts index 468caf93ec5dd..9085fb3cbc876 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.test.ts @@ -5,33 +5,27 @@ */ import { cryptoFactory } from '../../../server/lib/crypto'; -import { createMockServer } from '../../../test_helpers'; import { Logger } from '../../../types'; import { decryptJobHeaders } from './decrypt_job_headers'; -let mockServer: any; -beforeEach(() => { - mockServer = createMockServer(''); -}); - -const encryptHeaders = async (headers: Record) => { - const crypto = cryptoFactory(mockServer); +const encryptHeaders = async (encryptionKey: string, headers: Record) => { + const crypto = cryptoFactory(encryptionKey); return await crypto.encrypt(headers); }; describe('headers', () => { test(`fails if it can't decrypt headers`, async () => { - await expect( + const getDecryptedHeaders = () => decryptJobHeaders({ + encryptionKey: 'abcsecretsauce', job: { headers: 'Q53+9A+zf+Xe+ceR/uB/aR/Sw/8e+M+qR+WiG+8z+EY+mo+HiU/zQL+Xn', }, logger: ({ error: jest.fn(), } as unknown) as Logger, - server: mockServer, - }) - ).rejects.toMatchInlineSnapshot( + }); + await expect(getDecryptedHeaders()).rejects.toMatchInlineSnapshot( `[Error: Failed to decrypt report job data. Please ensure that xpack.reporting.encryptionKey is set and re-generate this report. Error: Invalid IV length]` ); }); @@ -42,15 +36,15 @@ describe('headers', () => { baz: 'quix', }; - const encryptedHeaders = await encryptHeaders(headers); + const encryptedHeaders = await encryptHeaders('abcsecretsauce', headers); const decryptedHeaders = await decryptJobHeaders({ + encryptionKey: 'abcsecretsauce', job: { title: 'cool-job-bro', type: 'csv', headers: encryptedHeaders, }, logger: {} as Logger, - server: mockServer, }); expect(decryptedHeaders).toEqual(headers); }); diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts index 436b2c2dab1ad..6f415d7ee5ea9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/decrypt_job_headers.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { cryptoFactory } from '../../../server/lib/crypto'; -import { CryptoFactory, ServerFacade, Logger } from '../../../types'; +import { CryptoFactory, Logger } from '../../../types'; interface HasEncryptedHeaders { headers?: string; @@ -17,15 +17,15 @@ export const decryptJobHeaders = async < JobParamsType, JobDocPayloadType extends HasEncryptedHeaders >({ - server, + encryptionKey, job, logger, }: { - server: ServerFacade; + encryptionKey?: string; job: JobDocPayloadType; logger: Logger; }): Promise> => { - const crypto: CryptoFactory = cryptoFactory(server); + const crypto: CryptoFactory = cryptoFactory(encryptionKey); try { const decryptedHeaders: Record = await crypto.decrypt(job.headers); return decryptedHeaders; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.test.ts index eedb742ad7597..5f5fc94eee830 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.test.ts @@ -4,27 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMockReportingCore, createMockServer } from '../../../test_helpers'; -import { ReportingCore } from '../../../server'; +import sinon from 'sinon'; +import { createMockReportingCore } from '../../../test_helpers'; +import { ReportingConfig, ReportingCore } from '../../../server/types'; import { JobDocPayload } from '../../../types'; import { JobDocPayloadPDF } from '../../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './index'; +let mockConfig: ReportingConfig; let mockReportingPlugin: ReportingCore; -let mockServer: any; + +const getMockConfig = (mockConfigGet: sinon.SinonStub) => ({ + get: mockConfigGet, + kbnConfig: { get: mockConfigGet }, +}); + beforeEach(async () => { - mockReportingPlugin = await createMockReportingCore(); - mockServer = createMockServer(''); + const mockConfigGet = sinon + .stub() + .withArgs('kibanaServer', 'hostname') + .returns('custom-hostname'); + mockConfig = getMockConfig(mockConfigGet); + mockReportingPlugin = await createMockReportingCore(mockConfig); }); describe('conditions', () => { test(`uses hostname from reporting config if set`, async () => { - const settings: any = { - 'xpack.reporting.kibanaServer.hostname': 'custom-hostname', - }; - - mockServer = createMockServer({ settings }); - const permittedHeaders = { foo: 'bar', baz: 'quix', @@ -33,121 +38,20 @@ describe('conditions', () => { const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayload, filteredHeaders: permittedHeaders, - server: mockServer, + config: mockConfig, }); expect(conditionalHeaders.conditions.hostname).toEqual( - mockServer.config().get('xpack.reporting.kibanaServer.hostname') + mockConfig.get('kibanaServer', 'hostname') ); - }); - - test(`uses hostname from server.config if reporting config not set`, async () => { - const permittedHeaders = { - foo: 'bar', - baz: 'quix', - }; - - const conditionalHeaders = await getConditionalHeaders({ - job: {} as JobDocPayload, - filteredHeaders: permittedHeaders, - server: mockServer, - }); - - expect(conditionalHeaders.conditions.hostname).toEqual(mockServer.config().get('server.host')); - }); - - test(`uses port from reporting config if set`, async () => { - const settings = { - 'xpack.reporting.kibanaServer.port': 443, - }; - - mockServer = createMockServer({ settings }); - - const permittedHeaders = { - foo: 'bar', - baz: 'quix', - }; - - const conditionalHeaders = await getConditionalHeaders({ - job: {} as JobDocPayload, - filteredHeaders: permittedHeaders, - server: mockServer, - }); - - expect(conditionalHeaders.conditions.port).toEqual( - mockServer.config().get('xpack.reporting.kibanaServer.port') + expect(conditionalHeaders.conditions.port).toEqual(mockConfig.get('kibanaServer', 'port')); + expect(conditionalHeaders.conditions.protocol).toEqual( + mockConfig.get('kibanaServer', 'protocol') ); - }); - - test(`uses port from server if reporting config not set`, async () => { - const permittedHeaders = { - foo: 'bar', - baz: 'quix', - }; - - const conditionalHeaders = await getConditionalHeaders({ - job: {} as JobDocPayload, - filteredHeaders: permittedHeaders, - server: mockServer, - }); - - expect(conditionalHeaders.conditions.port).toEqual(mockServer.config().get('server.port')); - }); - - test(`uses basePath from server config`, async () => { - const permittedHeaders = { - foo: 'bar', - baz: 'quix', - }; - - const conditionalHeaders = await getConditionalHeaders({ - job: {} as JobDocPayload, - filteredHeaders: permittedHeaders, - server: mockServer, - }); - expect(conditionalHeaders.conditions.basePath).toEqual( - mockServer.config().get('server.basePath') + mockConfig.kbnConfig.get('server', 'basePath') ); }); - - test(`uses protocol from reporting config if set`, async () => { - const settings = { - 'xpack.reporting.kibanaServer.protocol': 'https', - }; - - mockServer = createMockServer({ settings }); - - const permittedHeaders = { - foo: 'bar', - baz: 'quix', - }; - - const conditionalHeaders = await getConditionalHeaders({ - job: {} as JobDocPayload, - filteredHeaders: permittedHeaders, - server: mockServer, - }); - - expect(conditionalHeaders.conditions.protocol).toEqual( - mockServer.config().get('xpack.reporting.kibanaServer.protocol') - ); - }); - - test(`uses protocol from server.info`, async () => { - const permittedHeaders = { - foo: 'bar', - baz: 'quix', - }; - - const conditionalHeaders = await getConditionalHeaders({ - job: {} as JobDocPayload, - filteredHeaders: permittedHeaders, - server: mockServer, - }); - - expect(conditionalHeaders.conditions.protocol).toEqual(mockServer.info.protocol); - }); }); test('uses basePath from job when creating saved object service', async () => { @@ -161,14 +65,14 @@ test('uses basePath from job when creating saved object service', async () => { const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayload, filteredHeaders: permittedHeaders, - server: mockServer, + config: mockConfig, }); const jobBasePath = '/sbp/s/marketing'; await getCustomLogo({ reporting: mockReportingPlugin, job: { basePath: jobBasePath } as JobDocPayloadPDF, conditionalHeaders, - server: mockServer, + config: mockConfig, }); const getBasePath = mockGetSavedObjectsClient.mock.calls[0][0].getBasePath; @@ -179,6 +83,11 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav const mockGetSavedObjectsClient = jest.fn(); mockReportingPlugin.getSavedObjectsClient = mockGetSavedObjectsClient; + const mockConfigGet = sinon.stub(); + mockConfigGet.withArgs('kibanaServer', 'hostname').returns('localhost'); + mockConfigGet.withArgs('server', 'basePath').returns('/sbp'); + mockConfig = getMockConfig(mockConfigGet); + const permittedHeaders = { foo: 'bar', baz: 'quix', @@ -186,14 +95,14 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayload, filteredHeaders: permittedHeaders, - server: mockServer, + config: mockConfig, }); await getCustomLogo({ reporting: mockReportingPlugin, job: {} as JobDocPayloadPDF, conditionalHeaders, - server: mockServer, + config: mockConfig, }); const getBasePath = mockGetSavedObjectsClient.mock.calls[0][0].getBasePath; @@ -225,19 +134,26 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav describe('config formatting', () => { test(`lowercases server.host`, async () => { - mockServer = createMockServer({ settings: { 'server.host': 'COOL-HOSTNAME' } }); + const mockConfigGet = sinon + .stub() + .withArgs('server', 'host') + .returns('COOL-HOSTNAME'); + mockConfig = getMockConfig(mockConfigGet); + const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayload, filteredHeaders: {}, - server: mockServer, + config: mockConfig, }); expect(conditionalHeaders.conditions.hostname).toEqual('cool-hostname'); }); - test(`lowercases xpack.reporting.kibanaServer.hostname`, async () => { - mockServer = createMockServer({ - settings: { 'xpack.reporting.kibanaServer.hostname': 'GREAT-HOSTNAME' }, - }); + test(`lowercases kibanaServer.hostname`, async () => { + const mockConfigGet = sinon + .stub() + .withArgs('kibanaServer', 'hostname') + .returns('GREAT-HOSTNAME'); + mockConfig = getMockConfig(mockConfigGet); const conditionalHeaders = await getConditionalHeaders({ job: { title: 'cool-job-bro', @@ -249,7 +165,7 @@ describe('config formatting', () => { }, }, filteredHeaders: {}, - server: mockServer, + config: mockConfig, }); expect(conditionalHeaders.conditions.hostname).toEqual('great-hostname'); }); diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.ts index 975060a8052f0..bd7999d697ca9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_conditional_headers.ts @@ -3,29 +3,31 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ConditionalHeaders, ServerFacade } from '../../../types'; + +import { ReportingConfig } from '../../../server/types'; +import { ConditionalHeaders } from '../../../types'; export const getConditionalHeaders = ({ - server, + config, job, filteredHeaders, }: { - server: ServerFacade; + config: ReportingConfig; job: JobDocPayloadType; filteredHeaders: Record; }) => { - const config = server.config(); + const { kbnConfig } = config; const [hostname, port, basePath, protocol] = [ - config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'), - config.get('xpack.reporting.kibanaServer.port') || config.get('server.port'), - config.get('server.basePath'), - config.get('xpack.reporting.kibanaServer.protocol') || server.info.protocol, + config.get('kibanaServer', 'hostname'), + config.get('kibanaServer', 'port'), + kbnConfig.get('server', 'basePath'), + config.get('kibanaServer', 'protocol'), ] as [string, number, string, string]; const conditionalHeaders: ConditionalHeaders = { headers: filteredHeaders, conditions: { - hostname: hostname.toLowerCase(), + hostname: hostname ? hostname.toLowerCase() : hostname, port, basePath, protocol, diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.test.ts index fa53f474dfba7..2cbde69c81316 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.test.ts @@ -5,16 +5,18 @@ */ import { ReportingCore } from '../../../server'; -import { createMockReportingCore, createMockServer } from '../../../test_helpers'; -import { ServerFacade } from '../../../types'; +import { createMockReportingCore } from '../../../test_helpers'; import { JobDocPayloadPDF } from '../../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './index'; +const mockConfigGet = jest.fn().mockImplementation((key: string) => { + return 'localhost'; +}); +const mockConfig = { get: mockConfigGet, kbnConfig: { get: mockConfigGet } }; + let mockReportingPlugin: ReportingCore; -let mockServer: ServerFacade; beforeEach(async () => { - mockReportingPlugin = await createMockReportingCore(); - mockServer = createMockServer(''); + mockReportingPlugin = await createMockReportingCore(mockConfig); }); test(`gets logo from uiSettings`, async () => { @@ -37,14 +39,14 @@ test(`gets logo from uiSettings`, async () => { const conditionalHeaders = await getConditionalHeaders({ job: {} as JobDocPayloadPDF, filteredHeaders: permittedHeaders, - server: mockServer, + config: mockConfig, }); const { logo } = await getCustomLogo({ reporting: mockReportingPlugin, + config: mockConfig, job: {} as JobDocPayloadPDF, conditionalHeaders, - server: mockServer, }); expect(mockGet).toBeCalledWith('xpackReporting:customPdfLogo'); diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts index 7af5edab41ab7..a13f992e7867c 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_custom_logo.ts @@ -5,23 +5,22 @@ */ import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants'; -import { ReportingCore } from '../../../server'; -import { ConditionalHeaders, ServerFacade } from '../../../types'; +import { ReportingConfig, ReportingCore } from '../../../server/types'; +import { ConditionalHeaders } from '../../../types'; import { JobDocPayloadPDF } from '../../printable_pdf/types'; // Logo is PDF only export const getCustomLogo = async ({ reporting, - server, + config, job, conditionalHeaders, }: { reporting: ReportingCore; - server: ServerFacade; + config: ReportingConfig; job: JobDocPayloadPDF; conditionalHeaders: ConditionalHeaders; }) => { - const serverBasePath: string = server.config().get('server.basePath'); - + const serverBasePath: string = config.kbnConfig.get('server', 'basePath'); const fakeRequest: any = { headers: conditionalHeaders.headers, // This is used by the spaces SavedObjectClientWrapper to determine the existing space. diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.test.ts index 27e772195f726..5f55617724ff6 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.test.ts @@ -4,29 +4,41 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMockServer } from '../../../test_helpers'; -import { ServerFacade } from '../../../types'; +import { ReportingConfig } from '../../../server'; import { JobDocPayloadPNG } from '../../png/types'; import { JobDocPayloadPDF } from '../../printable_pdf/types'; import { getFullUrls } from './get_full_urls'; interface FullUrlsOpts { job: JobDocPayloadPNG & JobDocPayloadPDF; - server: ServerFacade; - conditionalHeaders: any; + config: ReportingConfig; } -let mockServer: any; +let mockConfig: ReportingConfig; +const getMockConfig = (mockConfigGet: jest.Mock) => { + return { + get: mockConfigGet, + kbnConfig: { get: mockConfigGet }, + }; +}; + beforeEach(() => { - mockServer = createMockServer(''); + const reportingConfig: Record = { + 'kibanaServer.hostname': 'localhost', + 'kibanaServer.port': 5601, + 'kibanaServer.protocol': 'http', + 'server.basePath': '/sbp', + }; + const mockConfigGet = jest.fn().mockImplementation((...keys: string[]) => { + return reportingConfig[keys.join('.') as string]; + }); + mockConfig = getMockConfig(mockConfigGet); }); +const getMockJob = (base: object) => base as JobDocPayloadPNG & JobDocPayloadPDF; + test(`fails if no URL is passed`, async () => { - const fn = () => - getFullUrls({ - job: {}, - server: mockServer, - } as FullUrlsOpts); + const fn = () => getFullUrls({ job: getMockJob({}), config: mockConfig } as FullUrlsOpts); expect(fn).toThrowErrorMatchingInlineSnapshot( `"No valid URL fields found in Job Params! Expected \`job.relativeUrl: string\` or \`job.relativeUrls: string[]\`"` ); @@ -37,8 +49,8 @@ test(`fails if URLs are file-protocols for PNGs`, async () => { const relativeUrl = 'file://etc/passwd/#/something'; const fn = () => getFullUrls({ - job: { relativeUrl, forceNow }, - server: mockServer, + job: getMockJob({ relativeUrl, forceNow }), + config: mockConfig, } as FullUrlsOpts); expect(fn).toThrowErrorMatchingInlineSnapshot( `"Found invalid URL(s), all URLs must be relative: file://etc/passwd/#/something"` @@ -51,8 +63,8 @@ test(`fails if URLs are absolute for PNGs`, async () => { 'http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something'; const fn = () => getFullUrls({ - job: { relativeUrl, forceNow }, - server: mockServer, + job: getMockJob({ relativeUrl, forceNow }), + config: mockConfig, } as FullUrlsOpts); expect(fn).toThrowErrorMatchingInlineSnapshot( `"Found invalid URL(s), all URLs must be relative: http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something"` @@ -64,11 +76,11 @@ test(`fails if URLs are file-protocols for PDF`, async () => { const relativeUrl = 'file://etc/passwd/#/something'; const fn = () => getFullUrls({ - job: { + job: getMockJob({ relativeUrls: [relativeUrl], forceNow, - }, - server: mockServer, + }), + config: mockConfig, } as FullUrlsOpts); expect(fn).toThrowErrorMatchingInlineSnapshot( `"Found invalid URL(s), all URLs must be relative: file://etc/passwd/#/something"` @@ -81,11 +93,11 @@ test(`fails if URLs are absolute for PDF`, async () => { 'http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something'; const fn = () => getFullUrls({ - job: { + job: getMockJob({ relativeUrls: [relativeUrl], forceNow, - }, - server: mockServer, + }), + config: mockConfig, } as FullUrlsOpts); expect(fn).toThrowErrorMatchingInlineSnapshot( `"Found invalid URL(s), all URLs must be relative: http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something"` @@ -102,8 +114,8 @@ test(`fails if any URLs are absolute or file's for PDF`, async () => { const fn = () => getFullUrls({ - job: { relativeUrls, forceNow }, - server: mockServer, + job: getMockJob({ relativeUrls, forceNow }), + config: mockConfig, } as FullUrlsOpts); expect(fn).toThrowErrorMatchingInlineSnapshot( `"Found invalid URL(s), all URLs must be relative: http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something file://etc/passwd/#/something"` @@ -113,8 +125,8 @@ test(`fails if any URLs are absolute or file's for PDF`, async () => { test(`fails if URL does not route to a visualization`, async () => { const fn = () => getFullUrls({ - job: { relativeUrl: '/app/phoney' }, - server: mockServer, + job: getMockJob({ relativeUrl: '/app/phoney' }), + config: mockConfig, } as FullUrlsOpts); expect(fn).toThrowErrorMatchingInlineSnapshot( `"No valid hash in the URL! A hash is expected for the application to route to the intended visualization."` @@ -124,8 +136,8 @@ test(`fails if URL does not route to a visualization`, async () => { test(`adds forceNow to hash's query, if it exists`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; const urls = await getFullUrls({ - job: { relativeUrl: '/app/kibana#/something', forceNow }, - server: mockServer, + job: getMockJob({ relativeUrl: '/app/kibana#/something', forceNow }), + config: mockConfig, } as FullUrlsOpts); expect(urls[0]).toEqual( @@ -137,8 +149,8 @@ test(`appends forceNow to hash's query, if it exists`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; const urls = await getFullUrls({ - job: { relativeUrl: '/app/kibana#/something?_g=something', forceNow }, - server: mockServer, + job: getMockJob({ relativeUrl: '/app/kibana#/something?_g=something', forceNow }), + config: mockConfig, } as FullUrlsOpts); expect(urls[0]).toEqual( @@ -148,8 +160,8 @@ test(`appends forceNow to hash's query, if it exists`, async () => { test(`doesn't append forceNow query to url, if it doesn't exists`, async () => { const urls = await getFullUrls({ - job: { relativeUrl: '/app/kibana#/something' }, - server: mockServer, + job: getMockJob({ relativeUrl: '/app/kibana#/something' }), + config: mockConfig, } as FullUrlsOpts); expect(urls[0]).toEqual('http://localhost:5601/sbp/app/kibana#/something'); @@ -158,7 +170,7 @@ test(`doesn't append forceNow query to url, if it doesn't exists`, async () => { test(`adds forceNow to each of multiple urls`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; const urls = await getFullUrls({ - job: { + job: getMockJob({ relativeUrls: [ '/app/kibana#/something_aaa', '/app/kibana#/something_bbb', @@ -166,8 +178,8 @@ test(`adds forceNow to each of multiple urls`, async () => { '/app/kibana#/something_ddd', ], forceNow, - }, - server: mockServer, + }), + config: mockConfig, } as FullUrlsOpts); expect(urls).toEqual([ diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts index ca64d8632dbfe..c4b6f31019fdf 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts @@ -12,7 +12,7 @@ import { } from 'url'; import { getAbsoluteUrlFactory } from '../../../common/get_absolute_url'; import { validateUrls } from '../../../common/validate_urls'; -import { ServerFacade } from '../../../types'; +import { ReportingConfig } from '../../../server/types'; import { JobDocPayloadPNG } from '../../png/types'; import { JobDocPayloadPDF } from '../../printable_pdf/types'; @@ -24,19 +24,23 @@ function isPdfJob(job: JobDocPayloadPNG | JobDocPayloadPDF): job is JobDocPayloa } export function getFullUrls({ - server, + config, job, }: { - server: ServerFacade; + config: ReportingConfig; job: JobDocPayloadPDF | JobDocPayloadPNG; }) { - const config = server.config(); - + const [basePath, protocol, hostname, port] = [ + config.kbnConfig.get('server', 'basePath'), + config.get('kibanaServer', 'protocol'), + config.get('kibanaServer', 'hostname'), + config.get('kibanaServer', 'port'), + ] as string[]; const getAbsoluteUrl = getAbsoluteUrlFactory({ - defaultBasePath: config.get('server.basePath'), - protocol: config.get('xpack.reporting.kibanaServer.protocol') || server.info.protocol, - hostname: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'), - port: config.get('xpack.reporting.kibanaServer.port') || config.get('server.port'), + defaultBasePath: basePath, + protocol, + hostname, + port, }); // PDF and PNG job params put in the url differently diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.test.ts index f446369fec78c..abf5784dacff9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.test.ts @@ -19,6 +19,9 @@ test(`omits blacklisted headers`, async () => { 'content-type': '', host: '', 'transfer-encoding': '', + 'proxy-connection': 'bananas', + 'proxy-authorization': 'some-base64-encoded-thing', + trailer: 's are for trucks', }; const filteredHeaders = await omitBlacklistedHeaders({ diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.ts index cbebd6bc21b0e..2fbfd868674f6 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/omit_blacklisted_headers.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { omit } from 'lodash'; -import { KBN_SCREENSHOT_HEADER_BLACKLIST } from '../../../common/constants'; +import { + KBN_SCREENSHOT_HEADER_BLACKLIST, + KBN_SCREENSHOT_HEADER_BLACKLIST_STARTS_WITH_PATTERN, +} from '../../../common/constants'; export const omitBlacklistedHeaders = ({ job, @@ -15,7 +18,12 @@ export const omitBlacklistedHeaders = ({ }) => { const filteredHeaders: Record = omit( decryptedHeaders, - KBN_SCREENSHOT_HEADER_BLACKLIST + (_value, header: string) => + header && + (KBN_SCREENSHOT_HEADER_BLACKLIST.includes(header) || + KBN_SCREENSHOT_HEADER_BLACKLIST_STARTS_WITH_PATTERN.some(pattern => + header?.startsWith(pattern) + )) ); return filteredHeaders; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/create_layout.ts b/x-pack/legacy/plugins/reporting/export_types/common/layouts/create_layout.ts index 0cb83352d4606..07fceb603e451 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/create_layout.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/layouts/create_layout.ts @@ -3,17 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ServerFacade } from '../../../types'; + +import { CaptureConfig } from '../../../server/types'; import { LayoutTypes } from '../constants'; import { Layout, LayoutParams } from './layout'; import { PreserveLayout } from './preserve_layout'; import { PrintLayout } from './print_layout'; -export function createLayout(server: ServerFacade, layoutParams?: LayoutParams): Layout { +export function createLayout(captureConfig: CaptureConfig, layoutParams?: LayoutParams): Layout { if (layoutParams && layoutParams.id === LayoutTypes.PRESERVE_LAYOUT) { return new PreserveLayout(layoutParams.dimensions); } // this is the default because some jobs won't have anything specified - return new PrintLayout(server); + return new PrintLayout(captureConfig); } diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/print_layout.ts b/x-pack/legacy/plugins/reporting/export_types/common/layouts/print_layout.ts index 6007c2960057a..f6974379253fb 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/print_layout.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/layouts/print_layout.ts @@ -3,14 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import path from 'path'; import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; -import { LevelLogger } from '../../../server/lib'; import { HeadlessChromiumDriver } from '../../../server/browsers'; -import { ServerFacade } from '../../../types'; +import { LevelLogger } from '../../../server/lib'; +import { CaptureConfig } from '../../../server/types'; import { LayoutTypes } from '../constants'; import { getDefaultLayoutSelectors, Layout, LayoutSelectorDictionary, Size } from './layout'; -import { CaptureConfig } from './types'; export class PrintLayout extends Layout { public readonly selectors: LayoutSelectorDictionary = { @@ -20,9 +20,9 @@ export class PrintLayout extends Layout { public readonly groupCount = 2; private captureConfig: CaptureConfig; - constructor(server: ServerFacade) { + constructor(captureConfig: CaptureConfig) { super(LayoutTypes.PRINT); - this.captureConfig = server.config().get('xpack.reporting.capture'); + this.captureConfig = captureConfig; } public getCssOverridesPath() { diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts index 16eb433e8a75e..57d025890d3e2 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts @@ -7,17 +7,16 @@ import { i18n } from '@kbn/i18n'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; -import { ServerFacade } from '../../../../types'; +import { CaptureConfig } from '../../../../server/types'; import { LayoutInstance } from '../../layouts/layout'; import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants'; export const getNumberOfItems = async ( - server: ServerFacade, + captureConfig: CaptureConfig, browser: HeadlessBrowser, layout: LayoutInstance, logger: LevelLogger ): Promise => { - const config = server.config(); const { renderComplete: renderCompleteSelector, itemsCountAttribute } = layout.selectors; let itemsCount: number; @@ -33,7 +32,7 @@ export const getNumberOfItems = async ( // we have to use this hint to wait for all of them await browser.waitForSelector( `${renderCompleteSelector},[${itemsCountAttribute}]`, - { timeout: config.get('xpack.reporting.capture.timeouts.waitForElements') }, + { timeout: captureConfig.timeouts.waitForElements }, { context: CONTEXT_READMETADATA }, logger ); diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts index 13d07bcdd6baf..75ac3dca4ffa0 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.test.ts @@ -19,12 +19,9 @@ import * as Rx from 'rxjs'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { loggingServiceMock } from '../../../../../../../../src/core/server/mocks'; import { LevelLogger } from '../../../../server/lib'; -import { - createMockBrowserDriverFactory, - createMockLayoutInstance, - createMockServer, -} from '../../../../test_helpers'; +import { createMockBrowserDriverFactory, createMockLayoutInstance } from '../../../../test_helpers'; import { ConditionalHeaders, HeadlessChromiumDriver } from '../../../../types'; +import { CaptureConfig } from '../../../../server/types'; import { screenshotsObservableFactory } from './observable'; import { ElementsPositionAndAttribute } from './types'; @@ -34,8 +31,8 @@ import { ElementsPositionAndAttribute } from './types'; const mockLogger = jest.fn(loggingServiceMock.create); const logger = new LevelLogger(mockLogger()); -const __LEGACY = createMockServer({ settings: { 'xpack.reporting.capture': { loadDelay: 13 } } }); -const mockLayout = createMockLayoutInstance(__LEGACY); +const mockConfig = { timeouts: { openUrl: 13 } } as CaptureConfig; +const mockLayout = createMockLayoutInstance(mockConfig); /* * Tests @@ -48,7 +45,7 @@ describe('Screenshot Observable Pipeline', () => { }); it('pipelines a single url into screenshot and timeRange', async () => { - const getScreenshots$ = screenshotsObservableFactory(__LEGACY, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); const result = await getScreenshots$({ logger, urls: ['/welcome/home/start/index.htm'], @@ -86,7 +83,7 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(__LEGACY, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); const result = await getScreenshots$({ logger, urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php'], @@ -136,7 +133,7 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(__LEGACY, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); const getScreenshot = async () => { return await getScreenshots$({ logger, @@ -197,7 +194,7 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(__LEGACY, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); const getScreenshot = async () => { return await getScreenshots$({ logger, diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts index 44c04c763f840..53a11c18abd79 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts @@ -6,24 +6,22 @@ import * as Rx from 'rxjs'; import { catchError, concatMap, first, mergeMap, take, takeUntil, toArray } from 'rxjs/operators'; -import { CaptureConfig, HeadlessChromiumDriverFactory, ServerFacade } from '../../../../types'; +import { CaptureConfig } from '../../../../server/types'; +import { HeadlessChromiumDriverFactory } from '../../../../types'; import { getElementPositionAndAttributes } from './get_element_position_data'; import { getNumberOfItems } from './get_number_of_items'; import { getScreenshots } from './get_screenshots'; import { getTimeRange } from './get_time_range'; +import { injectCustomCss } from './inject_css'; import { openUrl } from './open_url'; import { ScreenSetupData, ScreenshotObservableOpts, ScreenshotResults } from './types'; import { waitForRenderComplete } from './wait_for_render'; import { waitForVisualizations } from './wait_for_visualizations'; -import { injectCustomCss } from './inject_css'; export function screenshotsObservableFactory( - server: ServerFacade, + captureConfig: CaptureConfig, browserDriverFactory: HeadlessChromiumDriverFactory ) { - const config = server.config(); - const captureConfig: CaptureConfig = config.get('xpack.reporting.capture'); - return function screenshotsObservable({ logger, urls, @@ -41,13 +39,13 @@ export function screenshotsObservableFactory( mergeMap(({ driver, exit$ }) => { const setup$: Rx.Observable = Rx.of(1).pipe( takeUntil(exit$), - mergeMap(() => openUrl(server, driver, url, conditionalHeaders, logger)), - mergeMap(() => getNumberOfItems(server, driver, layout, logger)), + mergeMap(() => openUrl(captureConfig, driver, url, conditionalHeaders, logger)), + mergeMap(() => getNumberOfItems(captureConfig, driver, layout, logger)), mergeMap(async itemsCount => { const viewport = layout.getViewport(itemsCount); await Promise.all([ driver.setViewport(viewport, logger), - waitForVisualizations(server, driver, itemsCount, layout, logger), + waitForVisualizations(captureConfig, driver, itemsCount, layout, logger), ]); }), mergeMap(async () => { @@ -60,7 +58,7 @@ export function screenshotsObservableFactory( await layout.positionElements(driver, logger); } - await waitForRenderComplete(driver, layout, captureConfig, logger); + await waitForRenderComplete(captureConfig, driver, layout, logger); }), mergeMap(async () => { return await Promise.all([ diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts index fbae1f91a7a6a..a484dfb243563 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts @@ -5,27 +5,26 @@ */ import { i18n } from '@kbn/i18n'; -import { ConditionalHeaders, ServerFacade } from '../../../../types'; -import { LevelLogger } from '../../../../server/lib'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; +import { LevelLogger } from '../../../../server/lib'; +import { CaptureConfig } from '../../../../server/types'; +import { ConditionalHeaders } from '../../../../types'; import { PAGELOAD_SELECTOR } from '../../constants'; export const openUrl = async ( - server: ServerFacade, + captureConfig: CaptureConfig, browser: HeadlessBrowser, url: string, conditionalHeaders: ConditionalHeaders, logger: LevelLogger ): Promise => { - const config = server.config(); - try { await browser.open( url, { conditionalHeaders, waitForSelector: PAGELOAD_SELECTOR, - timeout: config.get('xpack.reporting.capture.timeouts.openUrl'), + timeout: captureConfig.timeouts.openUrl, }, logger ); diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts index ab81a952f345c..76613c2d631d6 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElementPosition, ConditionalHeaders } from '../../../../types'; import { LevelLogger } from '../../../../server/lib'; +import { ConditionalHeaders, ElementPosition } from '../../../../types'; import { LayoutInstance } from '../../layouts/layout'; export interface ScreenshotObservableOpts { diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts index 2f6dc2829dfd8..069896c8d9e90 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts @@ -5,16 +5,16 @@ */ import { i18n } from '@kbn/i18n'; -import { CaptureConfig } from '../../../../types'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; +import { CaptureConfig } from '../../../../server/types'; import { LayoutInstance } from '../../layouts/layout'; import { CONTEXT_WAITFORRENDER } from './constants'; export const waitForRenderComplete = async ( + captureConfig: CaptureConfig, browser: HeadlessBrowser, layout: LayoutInstance, - captureConfig: CaptureConfig, logger: LevelLogger ) => { logger.debug( diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts index 93ad40026dff8..7960e1552e559 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { ServerFacade } from '../../../../types'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; +import { CaptureConfig } from '../../../../server/types'; import { LayoutInstance } from '../../layouts/layout'; import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; @@ -23,13 +23,12 @@ const getCompletedItemsCount = ({ renderCompleteSelector }: SelectorArgs) => { * 3. Wait for the render complete event to be fired once for each item */ export const waitForVisualizations = async ( - server: ServerFacade, + captureConfig: CaptureConfig, browser: HeadlessBrowser, itemsCount: number, layout: LayoutInstance, logger: LevelLogger ): Promise => { - const config = server.config(); const { renderComplete: renderCompleteSelector } = layout.selectors; logger.debug( @@ -45,7 +44,7 @@ export const waitForVisualizations = async ( fn: getCompletedItemsCount, args: [{ renderCompleteSelector }], toEqual: itemsCount, - timeout: config.get('xpack.reporting.capture.timeouts.renderComplete'), + timeout: captureConfig.timeouts.renderComplete, }, { context: CONTEXT_WAITFORELEMENTSTOBEINDOM }, logger diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts index 7ea67277015ab..0e704a041452a 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts @@ -11,14 +11,14 @@ import { CreateJobFactory, ESQueueCreateJobFn, RequestFacade, - ServerFacade, } from '../../../types'; import { JobParamsDiscoverCsv } from '../types'; export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore, server: ServerFacade) { - const crypto = cryptoFactory(server); +>> = function createJobFactoryFn(reporting: ReportingCore) { + const config = reporting.getConfig(); + const crypto = cryptoFactory(config.get('encryptionKey')); return async function createJob( jobParams: JobParamsDiscoverCsv, diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js index f12916b734dbf..93dbe598b367c 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js @@ -11,8 +11,8 @@ import { CancellationToken } from '../../../common/cancellation_token'; import { fieldFormats } from '../../../../../../../src/plugins/data/server'; import { createMockReportingCore } from '../../../test_helpers'; import { LevelLogger } from '../../../server/lib/level_logger'; -import { executeJobFactory } from './execute_job'; import { setFieldFormats } from '../../../server/services'; +import { executeJobFactory } from './execute_job'; const delay = ms => new Promise(resolve => setTimeout(() => resolve(), ms)); @@ -36,11 +36,12 @@ describe('CSV Execute Job', function() { let defaultElasticsearchResponse; let encryptedHeaders; - let cancellationToken; - let mockReportingPlugin; - let mockServer; let clusterStub; + let configGetStub; + let mockReportingConfig; + let mockReportingPlugin; let callAsCurrentUserStub; + let cancellationToken; const mockElasticsearch = { dataClient: { @@ -57,8 +58,16 @@ describe('CSV Execute Job', function() { }); beforeEach(async function() { - mockReportingPlugin = await createMockReportingCore(); - mockReportingPlugin.getUiSettingsServiceFactory = () => mockUiSettingsClient; + configGetStub = sinon.stub(); + configGetStub.withArgs('encryptionKey').returns(encryptionKey); + configGetStub.withArgs('csv', 'maxSizeBytes').returns(1024 * 1000); // 1mB + configGetStub.withArgs('csv', 'scroll').returns({}); + mockReportingConfig = { get: configGetStub, kbnConfig: { get: configGetStub } }; + + mockReportingPlugin = await createMockReportingCore(mockReportingConfig); + mockReportingPlugin.getUiSettingsServiceFactory = () => Promise.resolve(mockUiSettingsClient); + mockReportingPlugin.getElasticsearchService = () => Promise.resolve(mockElasticsearch); + cancellationToken = new CancellationToken(); defaultElasticsearchResponse = { @@ -75,7 +84,6 @@ describe('CSV Execute Job', function() { .stub(clusterStub, 'callAsCurrentUser') .resolves(defaultElasticsearchResponse); - const configGetStub = sinon.stub(); mockUiSettingsClient.get.withArgs('csv:separator').returns(','); mockUiSettingsClient.get.withArgs('csv:quoteValues').returns(true); @@ -93,36 +101,11 @@ describe('CSV Execute Job', function() { return fieldFormatsRegistry; }, }); - - mockServer = { - config: function() { - return { - get: configGetStub, - }; - }, - }; - mockServer - .config() - .get.withArgs('xpack.reporting.encryptionKey') - .returns(encryptionKey); - mockServer - .config() - .get.withArgs('xpack.reporting.csv.maxSizeBytes') - .returns(1024 * 1000); // 1mB - mockServer - .config() - .get.withArgs('xpack.reporting.csv.scroll') - .returns({}); }); describe('basic Elasticsearch call behavior', function() { it('should decrypt encrypted headers and pass to callAsCurrentUser', async function() { - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -138,12 +121,7 @@ describe('CSV Execute Job', function() { testBody: true, }; - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const job = { headers: encryptedHeaders, fields: [], @@ -170,12 +148,7 @@ describe('CSV Execute Job', function() { _scroll_id: scrollId, }); callAsCurrentUserStub.onSecondCall().resolves(defaultElasticsearchResponse); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -189,12 +162,7 @@ describe('CSV Execute Job', function() { }); it('should not execute scroll if there are no hits from the search', async function() { - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -224,12 +192,7 @@ describe('CSV Execute Job', function() { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -264,12 +227,7 @@ describe('CSV Execute Job', function() { _scroll_id: lastScrollId, }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -297,12 +255,7 @@ describe('CSV Execute Job', function() { _scroll_id: lastScrollId, }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -321,10 +274,7 @@ describe('CSV Execute Job', function() { describe('Cells with formula values', () => { it('returns `csv_contains_formulas` when cells contain formulas', async function() { - mockServer - .config() - .get.withArgs('xpack.reporting.csv.checkForFormulas') - .returns(true); + configGetStub.withArgs('csv', 'checkForFormulas').returns(true); callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: '=SUM(A1:A2)', two: 'bar' } }], @@ -332,12 +282,7 @@ describe('CSV Execute Job', function() { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -354,10 +299,7 @@ describe('CSV Execute Job', function() { }); it('returns warnings when headings contain formulas', async function() { - mockServer - .config() - .get.withArgs('xpack.reporting.csv.checkForFormulas') - .returns(true); + configGetStub.withArgs('csv', 'checkForFormulas').returns(true); callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { '=SUM(A1:A2)': 'foo', two: 'bar' } }], @@ -365,12 +307,7 @@ describe('CSV Execute Job', function() { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['=SUM(A1:A2)', 'two'], @@ -387,10 +324,7 @@ describe('CSV Execute Job', function() { }); it('returns no warnings when cells have no formulas', async function() { - mockServer - .config() - .get.withArgs('xpack.reporting.csv.checkForFormulas') - .returns(true); + configGetStub.withArgs('csv', 'checkForFormulas').returns(true); callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], @@ -398,12 +332,7 @@ describe('CSV Execute Job', function() { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -420,10 +349,7 @@ describe('CSV Execute Job', function() { }); it('returns no warnings when configured not to', async () => { - mockServer - .config() - .get.withArgs('xpack.reporting.csv.checkForFormulas') - .returns(false); + configGetStub.withArgs('csv', 'checkForFormulas').returns(false); callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: '=SUM(A1:A2)', two: 'bar' } }], @@ -431,12 +357,7 @@ describe('CSV Execute Job', function() { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -456,12 +377,7 @@ describe('CSV Execute Job', function() { describe('Elasticsearch call errors', function() { it('should reject Promise if search call errors out', async function() { callAsCurrentUserStub.rejects(new Error()); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -480,12 +396,7 @@ describe('CSV Execute Job', function() { _scroll_id: 'scrollId', }); callAsCurrentUserStub.onSecondCall().rejects(new Error()); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -506,12 +417,7 @@ describe('CSV Execute Job', function() { _scroll_id: undefined, }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -532,12 +438,7 @@ describe('CSV Execute Job', function() { _scroll_id: undefined, }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -565,12 +466,7 @@ describe('CSV Execute Job', function() { _scroll_id: undefined, }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -598,12 +494,7 @@ describe('CSV Execute Job', function() { _scroll_id: undefined, }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -639,12 +530,7 @@ describe('CSV Execute Job', function() { }); it('should stop calling Elasticsearch when cancellationToken.cancel is called', async function() { - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); executeJob( 'job345', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -659,12 +545,7 @@ describe('CSV Execute Job', function() { }); it(`shouldn't call clearScroll if it never got a scrollId`, async function() { - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); executeJob( 'job345', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -678,12 +559,7 @@ describe('CSV Execute Job', function() { }); it('should call clearScroll if it got a scrollId', async function() { - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); executeJob( 'job345', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -701,12 +577,7 @@ describe('CSV Execute Job', function() { describe('csv content', function() { it('should write column headers to output, even if there are no results', async function() { - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -718,12 +589,7 @@ describe('CSV Execute Job', function() { it('should use custom uiSettings csv:separator for header', async function() { mockUiSettingsClient.get.withArgs('csv:separator').returns(';'); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -735,12 +601,7 @@ describe('CSV Execute Job', function() { it('should escape column headers if uiSettings csv:quoteValues is true', async function() { mockUiSettingsClient.get.withArgs('csv:quoteValues').returns(true); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], @@ -752,12 +613,7 @@ describe('CSV Execute Job', function() { it(`shouldn't escape column headers if uiSettings csv:quoteValues is false`, async function() { mockUiSettingsClient.get.withArgs('csv:quoteValues').returns(false); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], @@ -768,12 +624,7 @@ describe('CSV Execute Job', function() { }); it('should write column headers to output, when there are results', async function() { - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ one: '1', two: '2' }], @@ -793,12 +644,7 @@ describe('CSV Execute Job', function() { }); it('should use comma separated values of non-nested fields from _source', async function() { - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], @@ -819,12 +665,7 @@ describe('CSV Execute Job', function() { }); it('should concatenate the hits from multiple responses', async function() { - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], @@ -852,12 +693,7 @@ describe('CSV Execute Job', function() { }); it('should use field formatters to format fields', async function() { - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], @@ -897,17 +733,9 @@ describe('CSV Execute Job', function() { let maxSizeReached; beforeEach(async function() { - mockServer - .config() - .get.withArgs('xpack.reporting.csv.maxSizeBytes') - .returns(1); - - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + configGetStub.withArgs('csv', 'maxSizeBytes').returns(1); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -935,17 +763,9 @@ describe('CSV Execute Job', function() { let maxSizeReached; beforeEach(async function() { - mockServer - .config() - .get.withArgs('xpack.reporting.csv.maxSizeBytes') - .returns(9); - - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + configGetStub.withArgs('csv', 'maxSizeBytes').returns(9); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -973,10 +793,7 @@ describe('CSV Execute Job', function() { let maxSizeReached; beforeEach(async function() { - mockServer - .config() - .get.withArgs('xpack.reporting.csv.maxSizeBytes') - .returns(9); + configGetStub.withArgs('csv', 'maxSizeBytes').returns(9); callAsCurrentUserStub.onFirstCall().returns({ hits: { @@ -985,12 +802,7 @@ describe('CSV Execute Job', function() { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -1020,10 +832,7 @@ describe('CSV Execute Job', function() { beforeEach(async function() { mockReportingPlugin.getUiSettingsServiceFactory = () => mockUiSettingsClient; - mockServer - .config() - .get.withArgs('xpack.reporting.csv.maxSizeBytes') - .returns(18); + configGetStub.withArgs('csv', 'maxSizeBytes').returns(18); callAsCurrentUserStub.onFirstCall().returns({ hits: { @@ -1032,12 +841,7 @@ describe('CSV Execute Job', function() { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -1065,10 +869,7 @@ describe('CSV Execute Job', function() { describe('scroll settings', function() { it('passes scroll duration to initial search call', async function() { const scrollDuration = 'test'; - mockServer - .config() - .get.withArgs('xpack.reporting.csv.scroll') - .returns({ duration: scrollDuration }); + configGetStub.withArgs('csv', 'scroll').returns({ duration: scrollDuration }); callAsCurrentUserStub.onFirstCall().returns({ hits: { @@ -1077,12 +878,7 @@ describe('CSV Execute Job', function() { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -1099,10 +895,7 @@ describe('CSV Execute Job', function() { it('passes scroll size to initial search call', async function() { const scrollSize = 100; - mockServer - .config() - .get.withArgs('xpack.reporting.csv.scroll') - .returns({ size: scrollSize }); + configGetStub.withArgs('csv', 'scroll').returns({ size: scrollSize }); callAsCurrentUserStub.onFirstCall().resolves({ hits: { @@ -1111,12 +904,7 @@ describe('CSV Execute Job', function() { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -1133,10 +921,7 @@ describe('CSV Execute Job', function() { it('passes scroll duration to subsequent scroll call', async function() { const scrollDuration = 'test'; - mockServer - .config() - .get.withArgs('xpack.reporting.csv.scroll') - .returns({ duration: scrollDuration }); + configGetStub.withArgs('csv', 'scroll').returns({ duration: scrollDuration }); callAsCurrentUserStub.onFirstCall().resolves({ hits: { @@ -1145,12 +930,7 @@ describe('CSV Execute Job', function() { _scroll_id: 'scrollId', }); - const executeJob = await executeJobFactory( - mockReportingPlugin, - mockServer, - mockElasticsearch, - mockLogger - ); + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts index 1579985891053..d78d8a8a8010d 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts @@ -6,38 +6,30 @@ import { i18n } from '@kbn/i18n'; import Hapi from 'hapi'; -import { - ElasticsearchServiceSetup, - IUiSettingsClient, - KibanaRequest, -} from '../../../../../../../src/core/server'; +import { IUiSettingsClient, KibanaRequest } from '../../../../../../../src/core/server'; import { CSV_JOB_TYPE } from '../../../common/constants'; -import { ReportingCore } from '../../../server'; +import { ReportingCore } from '../../../server/core'; import { cryptoFactory } from '../../../server/lib'; import { getFieldFormats } from '../../../server/services'; -import { ESQueueWorkerExecuteFn, ExecuteJobFactory, Logger, ServerFacade } from '../../../types'; +import { ESQueueWorkerExecuteFn, ExecuteJobFactory, Logger } from '../../../types'; import { JobDocPayloadDiscoverCsv } from '../types'; import { fieldFormatMapFactory } from './lib/field_format_map'; import { createGenerateCsv } from './lib/generate_csv'; export const executeJobFactory: ExecuteJobFactory> = async function executeJobFactoryFn( - reporting: ReportingCore, - server: ServerFacade, - elasticsearch: ElasticsearchServiceSetup, - parentLogger: Logger -) { - const crypto = cryptoFactory(server); - const config = server.config(); +>> = async function executeJobFactoryFn(reporting: ReportingCore, parentLogger: Logger) { + const config = reporting.getConfig(); + const crypto = cryptoFactory(config.get('encryptionKey')); const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job']); - const serverBasePath = config.get('server.basePath'); + const serverBasePath = config.kbnConfig.get('server', 'basePath'); return async function executeJob( jobId: string, job: JobDocPayloadDiscoverCsv, cancellationToken: any ) { + const elasticsearch = await reporting.getElasticsearchService(); const jobLogger = logger.clone([jobId]); const { @@ -131,9 +123,9 @@ export const executeJobFactory: ExecuteJobFactory) { const response = await request; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts b/x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts index 842330fa7c93f..529c195486bc6 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts @@ -5,7 +5,8 @@ */ import { CancellationToken } from '../../common/cancellation_token'; -import { JobDocPayload, JobParamPostPayload, ConditionalHeaders, RequestFacade } from '../../types'; +import { ScrollConfig } from '../../server/types'; +import { JobDocPayload, JobParamPostPayload } from '../../types'; interface DocValueField { field: string; @@ -106,7 +107,7 @@ export interface GenerateCsvParams { quoteValues: boolean; timezone: string | null; maxSizeBytes: number; - scroll: { duration: string; size: number }; + scroll: ScrollConfig; checkForFormulas?: boolean; }; } diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts index 17072d311b35f..8e0376a190267 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts @@ -5,18 +5,11 @@ */ import { notFound, notImplemented } from 'boom'; -import { ElasticsearchServiceSetup } from 'kibana/server'; import { get } from 'lodash'; import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../common/constants'; import { ReportingCore } from '../../../../server'; import { cryptoFactory } from '../../../../server/lib'; -import { - CreateJobFactory, - ImmediateCreateJobFn, - Logger, - RequestFacade, - ServerFacade, -} from '../../../../types'; +import { CreateJobFactory, ImmediateCreateJobFn, Logger, RequestFacade } from '../../../../types'; import { JobDocPayloadPanelCsv, JobParamsPanelCsv, @@ -37,13 +30,9 @@ interface VisData { export const createJobFactory: CreateJobFactory> = function createJobFactoryFn( - reporting: ReportingCore, - server: ServerFacade, - elasticsearch: ElasticsearchServiceSetup, - parentLogger: Logger -) { - const crypto = cryptoFactory(server); +>> = function createJobFactoryFn(reporting: ReportingCore, parentLogger: Logger) { + const config = reporting.getConfig(); + const crypto = cryptoFactory(config.get('encryptionKey')); const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'create-job']); return async function createJob( diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts index 6bb3e73fcfe84..afa917f17651c 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts @@ -5,7 +5,6 @@ */ import { i18n } from '@kbn/i18n'; -import { ElasticsearchServiceSetup } from 'kibana/server'; import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; import { ReportingCore } from '../../../server'; import { cryptoFactory } from '../../../server/lib'; @@ -15,7 +14,6 @@ import { JobDocOutput, Logger, RequestFacade, - ServerFacade, } from '../../../types'; import { CsvResultFromSearch } from '../../csv/types'; import { FakeRequest, JobDocPayloadPanelCsv, JobParamsPanelCsv, SearchPanel } from '../types'; @@ -23,15 +21,11 @@ import { createGenerateCsv } from './lib'; export const executeJobFactory: ExecuteJobFactory> = async function executeJobFactoryFn( - reporting: ReportingCore, - server: ServerFacade, - elasticsearch: ElasticsearchServiceSetup, - parentLogger: Logger -) { - const crypto = cryptoFactory(server); +>> = async function executeJobFactoryFn(reporting: ReportingCore, parentLogger: Logger) { + const config = reporting.getConfig(); + const crypto = cryptoFactory(config.get('encryptionKey')); const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'execute-job']); - const generateCsv = createGenerateCsv(reporting, server, elasticsearch, parentLogger); + const generateCsv = createGenerateCsv(reporting, parentLogger); return async function executeJob( jobId: string | null, @@ -57,11 +51,11 @@ export const executeJobFactory: ExecuteJobFactory; const serializedEncryptedHeaders = job.headers; try { decryptedHeaders = await crypto.decrypt(serializedEncryptedHeaders); @@ -79,10 +73,7 @@ export const executeJobFactory: ExecuteJobFactory { export async function generateCsvSearch( req: RequestFacade, reporting: ReportingCore, - server: ServerFacade, - elasticsearch: ElasticsearchServiceSetup, logger: Logger, searchPanel: SearchPanel, jobParams: JobParamsDiscoverCsv @@ -159,11 +153,12 @@ export async function generateCsvSearch( }, }; + const config = reporting.getConfig(); + const elasticsearch = await reporting.getElasticsearchService(); const { callAsCurrentUser } = elasticsearch.dataClient.asScoped( KibanaRequest.from(req.getRawRequest()) ); const callCluster = (...params: [string, object]) => callAsCurrentUser(...params); - const config = server.config(); const uiSettings = await getUiSettings(uiConfig); const generateCsvParams: GenerateCsvParams = { @@ -176,8 +171,8 @@ export async function generateCsvSearch( cancellationToken: new CancellationToken(), settings: { ...uiSettings, - maxSizeBytes: config.get('xpack.reporting.csv.maxSizeBytes'), - scroll: config.get('xpack.reporting.csv.scroll'), + maxSizeBytes: config.get('csv', 'maxSizeBytes'), + scroll: config.get('csv', 'scroll'), timezone, }, }; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/types.d.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/types.d.ts index 6a7d5f336e238..ab14d2dd8a660 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/types.d.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/types.d.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JobParamPostPayload, JobDocPayload, ServerFacade } from '../../types'; +import { JobDocPayload, JobParamPostPayload } from '../../types'; export interface FakeRequest { - headers: any; - server: ServerFacade; + headers: Record; } export interface JobParamsPostPayloadPanelCsv extends JobParamPostPayload { diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts index a6911e1f14704..1f834bde88a2d 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts @@ -12,14 +12,14 @@ import { CreateJobFactory, ESQueueCreateJobFn, RequestFacade, - ServerFacade, } from '../../../../types'; import { JobParamsPNG } from '../../types'; export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore, server: ServerFacade) { - const crypto = cryptoFactory(server); +>> = function createJobFactoryFn(reporting: ReportingCore) { + const config = reporting.getConfig(); + const crypto = cryptoFactory(config.get('encryptionKey')); return async function createJob( { objectType, title, relativeUrl, browserTimezone, layout }: JobParamsPNG, diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js index e2e6ba1b89096..cb63e7dad2fdf 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js @@ -5,7 +5,6 @@ */ import * as Rx from 'rxjs'; -import { memoize } from 'lodash'; import { createMockReportingCore } from '../../../../test_helpers'; import { cryptoFactory } from '../../../../server/lib/crypto'; import { executeJobFactory } from './index'; @@ -14,63 +13,65 @@ import { LevelLogger } from '../../../../server/lib'; jest.mock('../lib/generate_png', () => ({ generatePngObservableFactory: jest.fn() })); +let mockReporting; + const cancellationToken = { on: jest.fn(), }; -let config; -let mockServer; -let mockReporting; +const mockLoggerFactory = { + get: jest.fn().mockImplementation(() => ({ + error: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + })), +}; +const getMockLogger = () => new LevelLogger(mockLoggerFactory); -beforeEach(async () => { - mockReporting = await createMockReportingCore(); +const mockEncryptionKey = 'abcabcsecuresecret'; +const encryptHeaders = async headers => { + const crypto = cryptoFactory(mockEncryptionKey); + return await crypto.encrypt(headers); +}; - config = { - 'xpack.reporting.encryptionKey': 'testencryptionkey', +beforeEach(async () => { + const kbnConfig = { 'server.basePath': '/sbp', - 'server.host': 'localhost', - 'server.port': 5601, }; - mockServer = { - config: memoize(() => ({ get: jest.fn() })), - info: { - protocol: 'http', + const reportingConfig = { + encryptionKey: mockEncryptionKey, + 'kibanaServer.hostname': 'localhost', + 'kibanaServer.port': 5601, + 'kibanaServer.protocol': 'http', + }; + const mockReportingConfig = { + get: (...keys) => reportingConfig[keys.join('.')], + kbnConfig: { get: (...keys) => kbnConfig[keys.join('.')] }, + }; + + mockReporting = await createMockReportingCore(mockReportingConfig); + + const mockElasticsearch = { + dataClient: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), }, }; - mockServer.config().get.mockImplementation(key => { - return config[key]; - }); + const mockGetElasticsearch = jest.fn(); + mockGetElasticsearch.mockImplementation(() => Promise.resolve(mockElasticsearch)); + mockReporting.getElasticsearchService = mockGetElasticsearch; generatePngObservableFactory.mockReturnValue(jest.fn()); }); afterEach(() => generatePngObservableFactory.mockReset()); -const mockElasticsearch = { - dataClient: { - asScoped: () => ({ callAsCurrentUser: jest.fn() }), - }, -}; - -const getMockLogger = () => new LevelLogger(); - -const encryptHeaders = async headers => { - const crypto = cryptoFactory(mockServer); - return await crypto.encrypt(headers); -}; - test(`passes browserTimezone to generatePng`, async () => { const encryptedHeaders = await encryptHeaders({}); const generatePngObservable = generatePngObservableFactory(); generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const executeJob = await executeJobFactory( - mockReporting, - mockServer, - mockElasticsearch, - getMockLogger() - ); + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); const browserTimezone = 'UTC'; await executeJob( 'pngJobId', @@ -88,15 +89,7 @@ test(`passes browserTimezone to generatePng`, async () => { }); test(`returns content_type of application/png`, async () => { - const executeJob = await executeJobFactory( - mockReporting, - mockServer, - mockElasticsearch, - getMockLogger(), - { - browserDriverFactory: {}, - } - ); + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); const generatePngObservable = generatePngObservableFactory(); @@ -116,15 +109,7 @@ test(`returns content of generatePng getBuffer base64 encoded`, async () => { const generatePngObservable = generatePngObservableFactory(); generatePngObservable.mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); - const executeJob = await executeJobFactory( - mockReporting, - mockServer, - mockElasticsearch, - getMockLogger(), - { - browserDriverFactory: {}, - } - ); + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); const { content } = await executeJob( 'pngJobId', diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts index 8670f0027af89..113da92d1862f 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts @@ -4,18 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchServiceSetup } from 'kibana/server'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PNG_JOB_TYPE } from '../../../../common/constants'; import { ReportingCore } from '../../../../server'; -import { - ESQueueWorkerExecuteFn, - ExecuteJobFactory, - JobDocOutput, - Logger, - ServerFacade, -} from '../../../../types'; +import { ESQueueWorkerExecuteFn, ExecuteJobFactory, JobDocOutput, Logger } from '../../../../types'; import { decryptJobHeaders, getConditionalHeaders, @@ -29,22 +22,23 @@ type QueuedPngExecutorFactory = ExecuteJobFactory = Rx.of(1).pipe( - mergeMap(() => decryptJobHeaders({ server, job, logger })), + mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })), map(decryptedHeaders => omitBlacklistedHeaders({ job, decryptedHeaders })), - map(filteredHeaders => getConditionalHeaders({ server, job, filteredHeaders })), + map(filteredHeaders => getConditionalHeaders({ config, job, filteredHeaders })), mergeMap(conditionalHeaders => { - const urls = getFullUrls({ server, job }); + const urls = getFullUrls({ config, job }); const hashUrl = urls[0]; return generatePngObservable( jobLogger, diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts index 88e91982adc63..a15541d99f6fb 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts @@ -7,17 +7,18 @@ import * as Rx from 'rxjs'; import { map } from 'rxjs/operators'; import { LevelLogger } from '../../../../server/lib'; -import { ConditionalHeaders, HeadlessChromiumDriverFactory, ServerFacade } from '../../../../types'; +import { CaptureConfig } from '../../../../server/types'; +import { ConditionalHeaders, HeadlessChromiumDriverFactory } from '../../../../types'; import { LayoutParams } from '../../../common/layouts/layout'; import { PreserveLayout } from '../../../common/layouts/preserve_layout'; import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; import { ScreenshotResults } from '../../../common/lib/screenshots/types'; export function generatePngObservableFactory( - server: ServerFacade, + captureConfig: CaptureConfig, browserDriverFactory: HeadlessChromiumDriverFactory ) { - const screenshotsObservable = screenshotsObservableFactory(server, browserDriverFactory); + const screenshotsObservable = screenshotsObservableFactory(captureConfig, browserDriverFactory); return function generatePngObservable( logger: LevelLogger, diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.ts index 656c99991e1f6..25d2d64b1029d 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.ts @@ -12,14 +12,14 @@ import { CreateJobFactory, ESQueueCreateJobFn, RequestFacade, - ServerFacade, } from '../../../../types'; import { JobParamsPDF } from '../../types'; export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore, server: ServerFacade) { - const crypto = cryptoFactory(server); +>> = function createJobFactoryFn(reporting: ReportingCore) { + const config = reporting.getConfig(); + const crypto = cryptoFactory(config.get('encryptionKey')); return async function createJobFn( { title, relativeUrls, browserTimezone, layout, objectType }: JobParamsPDF, diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js index 484842ba18f2a..c6f07f8ad2d34 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js @@ -5,7 +5,6 @@ */ import * as Rx from 'rxjs'; -import { memoize } from 'lodash'; import { createMockReportingCore } from '../../../../test_helpers'; import { cryptoFactory } from '../../../../server/lib/crypto'; import { executeJobFactory } from './index'; @@ -14,57 +13,60 @@ import { LevelLogger } from '../../../../server/lib'; jest.mock('../lib/generate_pdf', () => ({ generatePdfObservableFactory: jest.fn() })); +let mockReporting; + const cancellationToken = { on: jest.fn(), }; -let config; -let mockServer; -let mockReporting; +const mockLoggerFactory = { + get: jest.fn().mockImplementation(() => ({ + error: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + })), +}; +const getMockLogger = () => new LevelLogger(mockLoggerFactory); -beforeEach(async () => { - mockReporting = await createMockReportingCore(); +const mockEncryptionKey = 'testencryptionkey'; +const encryptHeaders = async headers => { + const crypto = cryptoFactory(mockEncryptionKey); + return await crypto.encrypt(headers); +}; - config = { - 'xpack.reporting.encryptionKey': 'testencryptionkey', +beforeEach(async () => { + const kbnConfig = { 'server.basePath': '/sbp', - 'server.host': 'localhost', - 'server.port': 5601, }; - mockServer = { - config: memoize(() => ({ get: jest.fn() })), - info: { - protocol: 'http', + const reportingConfig = { + encryptionKey: mockEncryptionKey, + 'kibanaServer.hostname': 'localhost', + 'kibanaServer.port': 5601, + 'kibanaServer.protocol': 'http', + }; + const mockReportingConfig = { + get: (...keys) => reportingConfig[keys.join('.')], + kbnConfig: { get: (...keys) => kbnConfig[keys.join('.')] }, + }; + + mockReporting = await createMockReportingCore(mockReportingConfig); + + const mockElasticsearch = { + dataClient: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), }, }; - mockServer.config().get.mockImplementation(key => { - return config[key]; - }); + const mockGetElasticsearch = jest.fn(); + mockGetElasticsearch.mockImplementation(() => Promise.resolve(mockElasticsearch)); + mockReporting.getElasticsearchService = mockGetElasticsearch; generatePdfObservableFactory.mockReturnValue(jest.fn()); }); afterEach(() => generatePdfObservableFactory.mockReset()); -const getMockLogger = () => new LevelLogger(); -const mockElasticsearch = { - dataClient: { - asScoped: () => ({ callAsCurrentUser: jest.fn() }), - }, -}; - -const encryptHeaders = async headers => { - const crypto = cryptoFactory(mockServer); - return await crypto.encrypt(headers); -}; - test(`returns content_type of application/pdf`, async () => { - const executeJob = await executeJobFactory( - mockReporting, - mockServer, - mockElasticsearch, - getMockLogger() - ); + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); const generatePdfObservable = generatePdfObservableFactory(); @@ -84,12 +86,7 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); - const executeJob = await executeJobFactory( - mockReporting, - mockServer, - mockElasticsearch, - getMockLogger() - ); + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); const { content } = await executeJob( 'pdfJobId', diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts index 535c2dcd439a7..dbdccb6160a6e 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts @@ -4,18 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchServiceSetup } from 'kibana/server'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PDF_JOB_TYPE } from '../../../../common/constants'; import { ReportingCore } from '../../../../server'; -import { - ESQueueWorkerExecuteFn, - ExecuteJobFactory, - JobDocOutput, - Logger, - ServerFacade, -} from '../../../../types'; +import { ESQueueWorkerExecuteFn, ExecuteJobFactory, JobDocOutput, Logger } from '../../../../types'; import { decryptJobHeaders, getConditionalHeaders, @@ -30,23 +23,26 @@ type QueuedPdfExecutorFactory = ExecuteJobFactory = Rx.of(1).pipe( - mergeMap(() => decryptJobHeaders({ server, job, logger })), + mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })), map(decryptedHeaders => omitBlacklistedHeaders({ job, decryptedHeaders })), - map(filteredHeaders => getConditionalHeaders({ server, job, filteredHeaders })), - mergeMap(conditionalHeaders => getCustomLogo({ reporting, server, job, conditionalHeaders })), + map(filteredHeaders => getConditionalHeaders({ config, job, filteredHeaders })), + mergeMap(conditionalHeaders => getCustomLogo({ reporting, config, job, conditionalHeaders })), mergeMap(({ logo, conditionalHeaders }) => { - const urls = getFullUrls({ server, job }); + const urls = getFullUrls({ config, job }); const { browserTimezone, layout, title } = job; return generatePdfObservable( diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts index d78effaa1fc2f..a62b7ec7013a5 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts @@ -8,7 +8,8 @@ import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap } from 'rxjs/operators'; import { LevelLogger } from '../../../../server/lib'; -import { ConditionalHeaders, HeadlessChromiumDriverFactory, ServerFacade } from '../../../../types'; +import { CaptureConfig } from '../../../../server/types'; +import { ConditionalHeaders, HeadlessChromiumDriverFactory } from '../../../../types'; import { createLayout } from '../../../common/layouts'; import { LayoutInstance, LayoutParams } from '../../../common/layouts/layout'; import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; @@ -27,10 +28,10 @@ const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { }; export function generatePdfObservableFactory( - server: ServerFacade, + captureConfig: CaptureConfig, browserDriverFactory: HeadlessChromiumDriverFactory ) { - const screenshotsObservable = screenshotsObservableFactory(server, browserDriverFactory); + const screenshotsObservable = screenshotsObservableFactory(captureConfig, browserDriverFactory); return function generatePdfObservable( logger: LevelLogger, @@ -41,7 +42,7 @@ export function generatePdfObservableFactory( layoutParams: LayoutParams, logo?: string ): Rx.Observable<{ buffer: Buffer; warnings: string[] }> { - const layout = createLayout(server, layoutParams) as LayoutInstance; + const layout = createLayout(captureConfig, layoutParams) as LayoutInstance; const screenshots$ = screenshotsObservable({ logger, urls, diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/types.d.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/types.d.ts index 0a9dcfe986ca6..e8dd3c5207d92 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/types.d.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/types.d.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { JobDocPayload } from '../../types'; import { LayoutInstance, LayoutParams } from '../common/layouts/layout'; -import { JobDocPayload, ServerFacade, RequestFacade } from '../../types'; // Job params: structure of incoming user request data, after being parsed from RISON export interface JobParamsPDF { diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index 89e98302cddc9..a5d27d0545da1 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -12,9 +12,7 @@ import { config as reportingConfig } from './config'; import { legacyInit } from './server/legacy'; import { ReportingPluginSpecOptions } from './types'; -const kbToBase64Length = (kb: number) => { - return Math.floor((kb * 1024 * 8) / 6); -}; +const kbToBase64Length = (kb: number) => Math.floor((kb * 1024 * 8) / 6); export const reporting = (kibana: any) => { return new kibana.Plugin({ diff --git a/x-pack/legacy/plugins/reporting/log_configuration.ts b/x-pack/legacy/plugins/reporting/log_configuration.ts index b07475df6304f..7aaed2038bd52 100644 --- a/x-pack/legacy/plugins/reporting/log_configuration.ts +++ b/x-pack/legacy/plugins/reporting/log_configuration.ts @@ -6,22 +6,23 @@ import getosSync, { LinuxOs } from 'getos'; import { promisify } from 'util'; -import { ServerFacade, Logger } from './types'; +import { BROWSER_TYPE } from './common/constants'; +import { CaptureConfig } from './server/types'; +import { Logger } from './types'; const getos = promisify(getosSync); -export async function logConfiguration(server: ServerFacade, logger: Logger) { - const config = server.config(); +export async function logConfiguration(captureConfig: CaptureConfig, logger: Logger) { + const { + browser: { + type: browserType, + chromium: { disableSandbox }, + }, + } = captureConfig; - const browserType = config.get('xpack.reporting.capture.browser.type'); logger.debug(`Browser type: ${browserType}`); - - if (browserType === 'chromium') { - logger.debug( - `Chromium sandbox disabled: ${config.get( - 'xpack.reporting.capture.browser.chromium.disableSandbox' - )}` - ); + if (browserType === BROWSER_TYPE) { + logger.debug(`Chromium sandbox disabled: ${disableSandbox}`); } const os = await getos(); diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/args.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/args.ts index dc79a6b9db2c1..a2f7a1f3ad0da 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/args.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/args.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BrowserConfig } from '../../../../types'; +import { CaptureConfig } from '../../../../server/types'; + +type ViewportConfig = CaptureConfig['viewport']; +type BrowserConfig = CaptureConfig['browser']['chromium']; interface LaunchArgs { userDataDir: BrowserConfig['userDataDir']; - viewport: BrowserConfig['viewport']; + viewport: ViewportConfig; disableSandbox: BrowserConfig['disableSandbox']; proxy: BrowserConfig['proxy']; } diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index f90f2c7aee395..cb228150efbcd 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -19,7 +19,8 @@ import { import * as Rx from 'rxjs'; import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; -import { BrowserConfig, CaptureConfig } from '../../../../types'; +import { BROWSER_TYPE } from '../../../../common/constants'; +import { CaptureConfig } from '../../../../server/types'; import { LevelLogger as Logger } from '../../../lib/level_logger'; import { safeChildProcess } from '../../safe_child_process'; import { HeadlessChromiumDriver } from '../driver'; @@ -28,7 +29,8 @@ import { puppeteerLaunch } from '../puppeteer'; import { args } from './args'; type binaryPath = string; -type ViewportConfig = BrowserConfig['viewport']; +type BrowserConfig = CaptureConfig['browser']['chromium']; +type ViewportConfig = CaptureConfig['viewport']; export class HeadlessChromiumDriverFactory { private binaryPath: binaryPath; @@ -37,15 +39,10 @@ export class HeadlessChromiumDriverFactory { private userDataDir: string; private getChromiumArgs: (viewport: ViewportConfig) => string[]; - constructor( - binaryPath: binaryPath, - logger: Logger, - browserConfig: BrowserConfig, - captureConfig: CaptureConfig - ) { + constructor(binaryPath: binaryPath, logger: Logger, captureConfig: CaptureConfig) { this.binaryPath = binaryPath; - this.browserConfig = browserConfig; this.captureConfig = captureConfig; + this.browserConfig = captureConfig.browser.chromium; this.userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chromium-')); this.getChromiumArgs = (viewport: ViewportConfig) => @@ -57,7 +54,7 @@ export class HeadlessChromiumDriverFactory { }); } - type = 'chromium'; + type = BROWSER_TYPE; test(logger: Logger) { const chromiumArgs = args({ @@ -153,7 +150,7 @@ export class HeadlessChromiumDriverFactory { // HeadlessChromiumDriver: object to "drive" a browser page const driver = new HeadlessChromiumDriver(page, { - inspect: this.browserConfig.inspect, + inspect: !!this.browserConfig.inspect, networkPolicy: this.captureConfig.networkPolicy, }); diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/index.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/index.ts index d32338ae3e311..5f89662c94da2 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/index.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BrowserConfig, CaptureConfig } from '../../../types'; +import { CaptureConfig } from '../../../server/types'; import { LevelLogger } from '../../lib'; import { HeadlessChromiumDriverFactory } from './driver_factory'; @@ -13,8 +13,7 @@ export { paths } from './paths'; export async function createDriverFactory( binaryPath: string, logger: LevelLogger, - browserConfig: BrowserConfig, captureConfig: CaptureConfig ): Promise { - return new HeadlessChromiumDriverFactory(binaryPath, logger, browserConfig, captureConfig); + return new HeadlessChromiumDriverFactory(binaryPath, logger, captureConfig); } diff --git a/x-pack/legacy/plugins/reporting/server/browsers/create_browser_driver_factory.ts b/x-pack/legacy/plugins/reporting/server/browsers/create_browser_driver_factory.ts index 49c6222c9f276..af3b86919dc50 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/create_browser_driver_factory.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/create_browser_driver_factory.ts @@ -4,24 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from '../../types'; +import { ReportingConfig } from '../types'; +import { HeadlessChromiumDriverFactory } from './chromium/driver_factory'; import { ensureBrowserDownloaded } from './download'; -import { installBrowser } from './install'; -import { ServerFacade, CaptureConfig, Logger } from '../../types'; -import { BROWSER_TYPE } from '../../common/constants'; import { chromium } from './index'; -import { HeadlessChromiumDriverFactory } from './chromium/driver_factory'; +import { installBrowser } from './install'; export async function createBrowserDriverFactory( - server: ServerFacade, + config: ReportingConfig, logger: Logger ): Promise { - const config = server.config(); - - const dataDir: string = config.get('path.data'); - const captureConfig: CaptureConfig = config.get('xpack.reporting.capture'); - const browserType = captureConfig.browser.type; + const captureConfig = config.get('capture'); + const browserConfig = captureConfig.browser.chromium; const browserAutoDownload = captureConfig.browser.autoDownload; - const browserConfig = captureConfig.browser[BROWSER_TYPE]; + const browserType = captureConfig.browser.type; + const dataDir = config.kbnConfig.get('path', 'data'); if (browserConfig.disableSandbox) { logger.warning(`Enabling the Chromium sandbox provides an additional layer of protection.`); @@ -32,7 +30,7 @@ export async function createBrowserDriverFactory( try { const { binaryPath } = await installBrowser(logger, chromium, dataDir); - return chromium.createDriverFactory(binaryPath, logger, browserConfig, captureConfig); + return chromium.createDriverFactory(binaryPath, logger, captureConfig); } catch (error) { if (error.cause && ['EACCES', 'EEXIST'].includes(error.cause.code)) { logger.error( diff --git a/x-pack/legacy/plugins/reporting/server/browsers/download/ensure_downloaded.ts b/x-pack/legacy/plugins/reporting/server/browsers/download/ensure_downloaded.ts index 73186966e3d2f..3697c4b86ce3c 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/download/ensure_downloaded.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/download/ensure_downloaded.ts @@ -4,16 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resolve as resolvePath } from 'path'; import { existsSync } from 'fs'; - +import { resolve as resolvePath } from 'path'; +import { BROWSER_TYPE } from '../../../common/constants'; import { chromium } from '../index'; -import { BrowserDownload, BrowserType } from '../types'; - +import { BrowserDownload } from '../types'; import { md5 } from './checksum'; -import { asyncMap } from './util'; -import { download } from './download'; import { clean } from './clean'; +import { download } from './download'; +import { asyncMap } from './util'; /** * Check for the downloaded archive of each requested browser type and @@ -21,7 +20,7 @@ import { clean } from './clean'; * @param {String} browserType * @return {Promise} */ -export async function ensureBrowserDownloaded(browserType: BrowserType) { +export async function ensureBrowserDownloaded(browserType = BROWSER_TYPE) { await ensureDownloaded([chromium]); } diff --git a/x-pack/legacy/plugins/reporting/server/browsers/network_policy.ts b/x-pack/legacy/plugins/reporting/server/browsers/network_policy.ts index b36345c08bfee..9714c5965a5db 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/network_policy.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/network_policy.ts @@ -6,12 +6,7 @@ import * as _ from 'lodash'; import { parse } from 'url'; - -interface FirewallRule { - allow: boolean; - host?: string; - protocol?: string; -} +import { NetworkPolicyRule } from '../../types'; const isHostMatch = (actualHost: string, ruleHost: string) => { const hostParts = actualHost.split('.').reverse(); @@ -20,7 +15,7 @@ const isHostMatch = (actualHost: string, ruleHost: string) => { return _.every(ruleParts, (part, idx) => part === hostParts[idx]); }; -export const allowRequest = (url: string, rules: FirewallRule[]) => { +export const allowRequest = (url: string, rules: NetworkPolicyRule[]) => { const parsed = parse(url); if (!rules.length) { diff --git a/x-pack/legacy/plugins/reporting/server/browsers/types.d.ts b/x-pack/legacy/plugins/reporting/server/browsers/types.d.ts index 0c480fc82752b..f096073ec2f5f 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/types.d.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/types.d.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export type BrowserType = 'chromium'; - export interface BrowserDownload { paths: { archivesPath: string; diff --git a/x-pack/legacy/plugins/reporting/server/config/index.ts b/x-pack/legacy/plugins/reporting/server/config/index.ts new file mode 100644 index 0000000000000..623d3c2015f3b --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/config/index.ts @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Legacy } from 'kibana'; +import { CoreSetup } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; +import crypto from 'crypto'; +import { get } from 'lodash'; +import { NetworkPolicy } from '../../types'; + +// make config.get() aware of the value type it returns +interface Config { + get(key1: Key1): BaseType[Key1]; + get( + key1: Key1, + key2: Key2 + ): BaseType[Key1][Key2]; + get< + Key1 extends keyof BaseType, + Key2 extends keyof BaseType[Key1], + Key3 extends keyof BaseType[Key1][Key2] + >( + key1: Key1, + key2: Key2, + key3: Key3 + ): BaseType[Key1][Key2][Key3]; + get< + Key1 extends keyof BaseType, + Key2 extends keyof BaseType[Key1], + Key3 extends keyof BaseType[Key1][Key2], + Key4 extends keyof BaseType[Key1][Key2][Key3] + >( + key1: Key1, + key2: Key2, + key3: Key3, + key4: Key4 + ): BaseType[Key1][Key2][Key3][Key4]; +} + +interface KbnServerConfigType { + path: { data: string }; + server: { + basePath: string; + host: string; + name: string; + port: number; + protocol: string; + uuid: string; + }; +} + +export interface ReportingConfig extends Config { + kbnConfig: Config; +} + +type BrowserType = 'chromium'; + +interface BrowserConfig { + inspect: boolean; + userDataDir: string; + viewport: { width: number; height: number }; + disableSandbox: boolean; + proxy: { + enabled: boolean; + server?: string; + bypass?: string[]; + }; +} + +interface CaptureConfig { + browser: { + type: BrowserType; + autoDownload: boolean; + chromium: BrowserConfig; + }; + maxAttempts: number; + networkPolicy: NetworkPolicy; + loadDelay: number; + timeouts: { + openUrl: number; + waitForElements: number; + renderComplete: number; + }; + viewport: any; + zoom: any; +} + +interface QueueConfig { + indexInterval: string; + pollEnabled: boolean; + pollInterval: number; + pollIntervalErrorMultiplier: number; + timeout: number; +} + +interface ScrollConfig { + duration: string; + size: number; +} + +export interface ReportingConfigType { + capture: CaptureConfig; + csv: { + scroll: ScrollConfig; + enablePanelActionDownload: boolean; + checkForFormulas: boolean; + maxSizeBytes: number; + }; + encryptionKey: string; + kibanaServer: any; + index: string; + queue: QueueConfig; + roles: any; +} + +const addConfigDefaults = ( + server: Legacy.Server, + core: CoreSetup, + baseConfig: ReportingConfigType +) => { + // encryption key + let encryptionKey = baseConfig.encryptionKey; + if (encryptionKey === undefined) { + server.log( + ['reporting', 'config', 'warning'], + i18n.translate('xpack.reporting.selfCheckEncryptionKey.warning', { + defaultMessage: + `Generating a random key for {setting}. To prevent pending reports ` + + `from failing on restart, please set {setting} in kibana.yml`, + values: { + setting: 'xpack.reporting.encryptionKey', + }, + }) + ); + encryptionKey = crypto.randomBytes(16).toString('hex'); + } + + const { kibanaServer: reportingServer } = baseConfig; + const serverInfo = core.http.getServerInfo(); + + // kibanaServer.hostname, default to server.host, don't allow "0" + let kibanaServerHostname = reportingServer.hostname ? reportingServer.hostname : serverInfo.host; + if (kibanaServerHostname === '0') { + server.log( + ['reporting', 'config', 'warning'], + i18n.translate('xpack.reporting.selfCheckHostname.warning', { + defaultMessage: + `Found 'server.host: "0"' in settings. This is incompatible with Reporting. ` + + `To enable Reporting to work, '{setting}: 0.0.0.0' is being automatically to the configuration. ` + + `You can change to 'server.host: 0.0.0.0' or add '{setting}: 0.0.0.0' in kibana.yml to prevent this message.`, + values: { + setting: 'xpack.reporting.kibanaServer.hostname', + }, + }) + ); + kibanaServerHostname = '0.0.0.0'; + } + + // kibanaServer.port, default to server.port + const kibanaServerPort = reportingServer.port + ? reportingServer.port + : serverInfo.port; // prettier-ignore + + // kibanaServer.protocol, default to server.protocol + const kibanaServerProtocol = reportingServer.protocol + ? reportingServer.protocol + : serverInfo.protocol; + + return { + ...baseConfig, + encryptionKey, + kibanaServer: { + hostname: kibanaServerHostname, + port: kibanaServerPort, + protocol: kibanaServerProtocol, + }, + }; +}; + +export const buildConfig = ( + core: CoreSetup, + server: Legacy.Server, + reportingConfig: ReportingConfigType +): ReportingConfig => { + const config = server.config(); + const { http } = core; + const serverInfo = http.getServerInfo(); + + const kbnConfig = { + path: { + data: config.get('path.data'), + }, + server: { + basePath: core.http.basePath.serverBasePath, + host: serverInfo.host, + name: serverInfo.name, + port: serverInfo.port, + uuid: core.uuid.getInstanceUuid(), + protocol: serverInfo.protocol, + }, + }; + + // spreading arguments as an array allows the return type to be known by the compiler + reportingConfig = addConfigDefaults(server, core, reportingConfig); + return { + get: (...keys: string[]) => get(reportingConfig, keys.join('.'), null), + kbnConfig: { + get: (...keys: string[]) => get(kbnConfig, keys.join('.'), null), + }, + }; +}; diff --git a/x-pack/legacy/plugins/reporting/server/core.ts b/x-pack/legacy/plugins/reporting/server/core.ts index 4506d41e4f5c3..9be61d091b00e 100644 --- a/x-pack/legacy/plugins/reporting/server/core.ts +++ b/x-pack/legacy/plugins/reporting/server/core.ts @@ -7,6 +7,7 @@ import * as Rx from 'rxjs'; import { first, mapTo } from 'rxjs/operators'; import { + ElasticsearchServiceSetup, IUiSettingsClient, KibanaRequest, SavedObjectsClient, @@ -19,20 +20,24 @@ import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; import { PLUGIN_ID } from '../common/constants'; import { EnqueueJobFn, ESQueueInstance, ReportingPluginSpecOptions, ServerFacade } from '../types'; import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory'; +import { ReportingConfig, ReportingConfigType } from './config'; import { checkLicenseFactory, getExportTypesRegistry, LevelLogger } from './lib'; import { registerRoutes } from './routes'; import { ReportingSetupDeps } from './types'; interface ReportingInternalSetup { browserDriverFactory: HeadlessChromiumDriverFactory; + elasticsearch: ElasticsearchServiceSetup; } interface ReportingInternalStart { + enqueueJob: EnqueueJobFn; + esqueue: ESQueueInstance; savedObjects: SavedObjectsServiceStart; uiSettings: UiSettingsServiceStart; - esqueue: ESQueueInstance; - enqueueJob: EnqueueJobFn; } +export { ReportingConfig, ReportingConfigType }; + export class ReportingCore { private pluginSetupDeps?: ReportingInternalSetup; private pluginStartDeps?: ReportingInternalStart; @@ -40,7 +45,7 @@ export class ReportingCore { private readonly pluginStart$ = new Rx.ReplaySubject(); private exportTypesRegistry = getExportTypesRegistry(); - constructor(private logger: LevelLogger) {} + constructor(private logger: LevelLogger, private config: ReportingConfig) {} legacySetup( xpackMainPlugin: XPackMainPlugin, @@ -48,14 +53,18 @@ export class ReportingCore { __LEGACY: ServerFacade, plugins: ReportingSetupDeps ) { + // legacy plugin status mirrorPluginStatus(xpackMainPlugin, reporting); + + // legacy license check const checkLicense = checkLicenseFactory(this.exportTypesRegistry); (xpackMainPlugin as any).status.once('green', () => { // Register a function that is called whenever the xpack info changes, // to re-compute the license check results for this plugin xpackMainPlugin.info.feature(PLUGIN_ID).registerLicenseCheckResultsGenerator(checkLicense); }); - // Reporting routes + + // legacy routes registerRoutes(this, __LEGACY, plugins, this.logger); } @@ -90,23 +99,31 @@ export class ReportingCore { return (await this.getPluginSetupDeps()).browserDriverFactory; } + public getConfig(): ReportingConfig { + return this.config; + } + /* - * Kibana core module dependencies + * Outside dependencies */ - private async getPluginSetupDeps() { + private async getPluginSetupDeps(): Promise { if (this.pluginSetupDeps) { return this.pluginSetupDeps; } return await this.pluginSetup$.pipe(first()).toPromise(); } - private async getPluginStartDeps() { + private async getPluginStartDeps(): Promise { if (this.pluginStartDeps) { return this.pluginStartDeps; } return await this.pluginStart$.pipe(first()).toPromise(); } + public async getElasticsearchService(): Promise { + return (await this.getPluginSetupDeps()).elasticsearch; + } + public async getSavedObjectsClient(fakeRequest: KibanaRequest): Promise { const { savedObjects } = await this.getPluginStartDeps(); return savedObjects.getScopedClient(fakeRequest) as SavedObjectsClient; diff --git a/x-pack/legacy/plugins/reporting/server/index.ts b/x-pack/legacy/plugins/reporting/server/index.ts index 24e2a954415d9..c564963e363cc 100644 --- a/x-pack/legacy/plugins/reporting/server/index.ts +++ b/x-pack/legacy/plugins/reporting/server/index.ts @@ -6,10 +6,11 @@ import { PluginInitializerContext } from 'src/core/server'; import { ReportingPlugin as Plugin } from './plugin'; +import { ReportingConfig, ReportingCore } from './core'; -export const plugin = (context: PluginInitializerContext) => { - return new Plugin(context); +export const plugin = (context: PluginInitializerContext, config: ReportingConfig) => { + return new Plugin(context, config); }; -export { ReportingCore } from './core'; export { ReportingPlugin } from './plugin'; +export { ReportingConfig, ReportingCore }; diff --git a/x-pack/legacy/plugins/reporting/server/legacy.ts b/x-pack/legacy/plugins/reporting/server/legacy.ts index 336ff5f4d2ee7..679b42aca6de5 100644 --- a/x-pack/legacy/plugins/reporting/server/legacy.ts +++ b/x-pack/legacy/plugins/reporting/server/legacy.ts @@ -3,10 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { Legacy } from 'kibana'; import { PluginInitializerContext } from 'src/core/server'; import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { ReportingPluginSpecOptions } from '../types'; +import { buildConfig } from './config'; import { plugin } from './index'; import { LegacySetup, ReportingStartDeps } from './types'; @@ -14,24 +16,31 @@ const buildLegacyDependencies = ( server: Legacy.Server, reportingPlugin: ReportingPluginSpecOptions ): LegacySetup => ({ - config: server.config, - info: server.info, route: server.route.bind(server), + config: server.config, plugins: { - elasticsearch: server.plugins.elasticsearch, xpack_main: server.plugins.xpack_main, reporting: reportingPlugin, }, }); +/* + * Starts the New Platform instance of Reporting using legacy dependencies + */ export const legacyInit = async ( server: Legacy.Server, - reportingPlugin: ReportingPluginSpecOptions + reportingLegacyPlugin: ReportingPluginSpecOptions ) => { - const coreSetup = server.newPlatform.setup.core; - const pluginInstance = plugin(server.newPlatform.coreContext as PluginInitializerContext); + const { core: coreSetup } = server.newPlatform.setup; + const legacyConfig = server.config(); + const reportingConfig = buildConfig(coreSetup, server, legacyConfig.get('xpack.reporting')); - const __LEGACY = buildLegacyDependencies(server, reportingPlugin); + const __LEGACY = buildLegacyDependencies(server, reportingLegacyPlugin); + + const pluginInstance = plugin( + server.newPlatform.coreContext as PluginInitializerContext, + reportingConfig + ); await pluginInstance.setup(coreSetup, { elasticsearch: coreSetup.elasticsearch, security: server.newPlatform.setup.plugins.security as SecurityPluginSetup, @@ -42,7 +51,6 @@ export const legacyInit = async ( // Schedule to call the "start" hook only after start dependencies are ready coreSetup.getStartServices().then(([core, plugins]) => pluginInstance.start(core, { - elasticsearch: coreSetup.elasticsearch, data: (plugins as ReportingStartDeps).data, __LEGACY, }) diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts index d593e4625cdf4..8230ee889ae05 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts @@ -4,22 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchServiceSetup } from 'kibana/server'; -import { ESQueueInstance, ServerFacade, QueueConfig, Logger } from '../../types'; +import { ESQueueInstance, Logger } from '../../types'; import { ReportingCore } from '../core'; +import { createTaggedLogger } from './create_tagged_logger'; // TODO remove createTaggedLogger once esqueue is removed +import { createWorkerFactory } from './create_worker'; // @ts-ignore import { Esqueue } from './esqueue'; -import { createWorkerFactory } from './create_worker'; -import { createTaggedLogger } from './create_tagged_logger'; // TODO remove createTaggedLogger once esqueue is removed export async function createQueueFactory( reporting: ReportingCore, - server: ServerFacade, - elasticsearch: ElasticsearchServiceSetup, logger: Logger ): Promise { - const queueConfig: QueueConfig = server.config().get('xpack.reporting.queue'); - const index = server.config().get('xpack.reporting.index'); + const config = reporting.getConfig(); + const queueConfig = config.get('queue'); + const index = config.get('index'); + const elasticsearch = await reporting.getElasticsearchService(); const queueOptions = { interval: queueConfig.indexInterval, @@ -33,7 +32,7 @@ export async function createQueueFactory( if (queueConfig.pollEnabled) { // create workers to poll the index for idle jobs waiting to be claimed and executed - const createWorker = createWorkerFactory(reporting, server, elasticsearch, logger); + const createWorker = createWorkerFactory(reporting, logger); await createWorker(queue); } else { logger.info( diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts b/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts index d4d913243e18d..ad8db3201844e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchServiceSetup } from 'kibana/server'; import * as sinon from 'sinon'; -import { ReportingCore } from '../../server'; +import { ReportingConfig, ReportingCore } from '../../server/types'; import { createMockReportingCore } from '../../test_helpers'; -import { ServerFacade } from '../../types'; import { createWorkerFactory } from './create_worker'; // @ts-ignore import { Esqueue } from './esqueue'; @@ -17,21 +15,15 @@ import { ClientMock } from './esqueue/__tests__/fixtures/legacy_elasticsearch'; import { ExportTypesRegistry } from './export_types_registry'; const configGetStub = sinon.stub(); -configGetStub.withArgs('xpack.reporting.queue').returns({ +configGetStub.withArgs('queue').returns({ pollInterval: 3300, pollIntervalErrorMultiplier: 10, }); -configGetStub.withArgs('server.name').returns('test-server-123'); -configGetStub.withArgs('server.uuid').returns('g9ymiujthvy6v8yrh7567g6fwzgzftzfr'); +configGetStub.withArgs('server', 'name').returns('test-server-123'); +configGetStub.withArgs('server', 'uuid').returns('g9ymiujthvy6v8yrh7567g6fwzgzftzfr'); const executeJobFactoryStub = sinon.stub(); - -const getMockServer = (): ServerFacade => { - return ({ - config: () => ({ get: configGetStub }), - } as unknown) as ServerFacade; -}; -const getMockLogger = jest.fn(); +const getMockLogger = sinon.stub(); const getMockExportTypesRegistry = ( exportTypes: any[] = [{ executeJobFactory: executeJobFactoryStub }] @@ -41,25 +33,22 @@ const getMockExportTypesRegistry = ( } as ExportTypesRegistry); describe('Create Worker', () => { + let mockReporting: ReportingCore; + let mockConfig: ReportingConfig; let queue: Esqueue; let client: ClientMock; - let mockReporting: ReportingCore; beforeEach(async () => { - mockReporting = await createMockReportingCore(); + mockConfig = { get: configGetStub, kbnConfig: { get: configGetStub } }; + mockReporting = await createMockReportingCore(mockConfig); + mockReporting.getExportTypesRegistry = () => getMockExportTypesRegistry(); client = new ClientMock(); queue = new Esqueue('reporting-queue', { client }); executeJobFactoryStub.reset(); }); test('Creates a single Esqueue worker for Reporting', async () => { - mockReporting.getExportTypesRegistry = () => getMockExportTypesRegistry(); - const createWorker = createWorkerFactory( - mockReporting, - getMockServer(), - {} as ElasticsearchServiceSetup, - getMockLogger() - ); + const createWorker = createWorkerFactory(mockReporting, getMockLogger()); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); await createWorker(queue); @@ -91,12 +80,7 @@ Object { { executeJobFactory: executeJobFactoryStub }, ]); mockReporting.getExportTypesRegistry = () => exportTypesRegistry; - const createWorker = createWorkerFactory( - mockReporting, - getMockServer(), - {} as ElasticsearchServiceSetup, - getMockLogger() - ); + const createWorker = createWorkerFactory(mockReporting, getMockLogger()); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); await createWorker(queue); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts index 3567712367608..16b8fbdb30fdd 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchServiceSetup } from 'kibana/server'; import { CancellationToken } from '../../common/cancellation_token'; import { PLUGIN_ID } from '../../common/constants'; +import { ReportingCore } from '../../server/types'; import { ESQueueInstance, ESQueueWorkerExecuteFn, @@ -15,25 +15,18 @@ import { JobDocPayload, JobSource, Logger, - QueueConfig, RequestFacade, - ServerFacade, } from '../../types'; -import { ReportingCore } from '../core'; // @ts-ignore untyped dependency import { events as esqueueEvents } from './esqueue'; -export function createWorkerFactory( - reporting: ReportingCore, - server: ServerFacade, - elasticsearch: ElasticsearchServiceSetup, - logger: Logger -) { +export function createWorkerFactory(reporting: ReportingCore, logger: Logger) { type JobDocPayloadType = JobDocPayload; - const config = server.config(); - const queueConfig: QueueConfig = config.get('xpack.reporting.queue'); - const kibanaName: string = config.get('server.name'); - const kibanaId: string = config.get('server.uuid'); + + const config = reporting.getConfig(); + const queueConfig = config.get('queue'); + const kibanaName = config.kbnConfig.get('server', 'name'); + const kibanaId = config.kbnConfig.get('server', 'uuid'); // Once more document types are added, this will need to be passed in return async function createWorker(queue: ESQueueInstance) { @@ -44,15 +37,14 @@ export function createWorkerFactory( > = new Map(); for (const exportType of reporting.getExportTypesRegistry().getAll() as Array< - ExportTypeDefinition + ExportTypeDefinition< + JobParamsType, + unknown, + unknown, + ImmediateExecuteFn | ESQueueWorkerExecuteFn + > >) { - // TODO: the executeJobFn should be unwrapped in the register method of the export types registry - const jobExecutor = await exportType.executeJobFactory( - reporting, - server, - elasticsearch, - logger - ); + const jobExecutor = await exportType.executeJobFactory(reporting, logger); // FIXME: does not "need" to be async jobExecutors.set(exportType.jobType, jobExecutor); } diff --git a/x-pack/legacy/plugins/reporting/server/lib/crypto.ts b/x-pack/legacy/plugins/reporting/server/lib/crypto.ts index dbc01fc947f8b..97876529ecfa7 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/crypto.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/crypto.ts @@ -5,12 +5,7 @@ */ import nodeCrypto from '@elastic/node-crypto'; -import { oncePerServer } from './once_per_server'; -import { ServerFacade } from '../../types'; -function cryptoFn(server: ServerFacade) { - const encryptionKey = server.config().get('xpack.reporting.encryptionKey'); +export function cryptoFactory(encryptionKey: string | undefined) { return nodeCrypto({ encryptionKey }); } - -export const cryptoFactory = oncePerServer(cryptoFn); diff --git a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts index c215bdc398904..5a062a693b468 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts @@ -5,22 +5,18 @@ */ import { get } from 'lodash'; -import { ElasticsearchServiceSetup } from 'kibana/server'; -// @ts-ignore -import { events as esqueueEvents } from './esqueue'; import { + ConditionalHeaders, EnqueueJobFn, ESQueueCreateJobFn, ImmediateCreateJobFn, Job, - ServerFacade, - RequestFacade, Logger, - CaptureConfig, - QueueConfig, - ConditionalHeaders, + RequestFacade, } from '../../types'; import { ReportingCore } from '../core'; +// @ts-ignore +import { events as esqueueEvents } from './esqueue'; interface ConfirmedJob { id: string; @@ -29,18 +25,13 @@ interface ConfirmedJob { _primary_term: number; } -export function enqueueJobFactory( - reporting: ReportingCore, - server: ServerFacade, - elasticsearch: ElasticsearchServiceSetup, - parentLogger: Logger -): EnqueueJobFn { +export function enqueueJobFactory(reporting: ReportingCore, parentLogger: Logger): EnqueueJobFn { const logger = parentLogger.clone(['queue-job']); - const config = server.config(); - const captureConfig: CaptureConfig = config.get('xpack.reporting.capture'); + const config = reporting.getConfig(); + const captureConfig = config.get('capture'); + const queueConfig = config.get('queue'); const browserType = captureConfig.browser.type; const maxAttempts = captureConfig.maxAttempts; - const queueConfig: QueueConfig = config.get('xpack.reporting.queue'); return async function enqueueJob( exportTypeId: string, @@ -58,13 +49,7 @@ export function enqueueJobFactory( throw new Error(`Export type ${exportTypeId} does not exist in the registry!`); } - // TODO: the createJobFn should be unwrapped in the register method of the export types registry - const createJob = exportType.createJobFactory( - reporting, - server, - elasticsearch, - logger - ) as CreateJobFn; + const createJob = exportType.createJobFactory(reporting, logger) as CreateJobFn; const payload = await createJob(jobParams, headers, request); const options = { diff --git a/x-pack/legacy/plugins/reporting/server/lib/get_user.ts b/x-pack/legacy/plugins/reporting/server/lib/get_user.ts index 49d5c568c3981..5e73fe77ecb79 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/get_user.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/get_user.ts @@ -6,10 +6,10 @@ import { Legacy } from 'kibana'; import { KibanaRequest } from '../../../../../../src/core/server'; -import { ServerFacade } from '../../types'; +import { Logger } from '../../types'; import { ReportingSetupDeps } from '../types'; -export function getUserFactory(server: ServerFacade, security: ReportingSetupDeps['security']) { +export function getUserFactory(security: ReportingSetupDeps['security'], logger: Logger) { /* * Legacy.Request because this is called from routing middleware */ diff --git a/x-pack/legacy/plugins/reporting/server/lib/index.ts b/x-pack/legacy/plugins/reporting/server/lib/index.ts index 0a2db749cb954..f5ccbe493a91f 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/index.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/index.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getExportTypesRegistry } from './export_types_registry'; export { checkLicenseFactory } from './check_license'; -export { LevelLogger } from './level_logger'; -export { cryptoFactory } from './crypto'; -export { oncePerServer } from './once_per_server'; -export { runValidations } from './validate'; export { createQueueFactory } from './create_queue'; +export { cryptoFactory } from './crypto'; export { enqueueJobFactory } from './enqueue_job'; +export { getExportTypesRegistry } from './export_types_registry'; +export { LevelLogger } from './level_logger'; +export { runValidations } from './validate'; diff --git a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts index c01e6377b039e..0affc111c1368 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts @@ -9,7 +9,8 @@ import Boom from 'boom'; import { errors as elasticsearchErrors } from 'elasticsearch'; import { ElasticsearchServiceSetup } from 'kibana/server'; import { get } from 'lodash'; -import { JobSource, ServerFacade } from '../../types'; +import { JobSource } from '../../types'; +import { ReportingConfig } from '../types'; const esErrors = elasticsearchErrors as Record; const defaultSize = 10; @@ -39,8 +40,11 @@ interface CountAggResult { count: number; } -export function jobsQueryFactory(server: ServerFacade, elasticsearch: ElasticsearchServiceSetup) { - const index = server.config().get('xpack.reporting.index'); +export function jobsQueryFactory( + config: ReportingConfig, + elasticsearch: ElasticsearchServiceSetup +) { + const index = config.get('index'); const { callAsInternalUser } = elasticsearch.adminClient; function getUsername(user: any) { diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_encryption_key.js b/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_encryption_key.js deleted file mode 100644 index 10980f702d849..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_encryption_key.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { validateEncryptionKey } from '../validate_encryption_key'; - -describe('Reporting: Validate config', () => { - const logger = { - warning: sinon.spy(), - }; - - beforeEach(() => { - logger.warning.resetHistory(); - }); - - [undefined, null].forEach(value => { - it(`should log a warning and set xpack.reporting.encryptionKey if encryptionKey is ${value}`, () => { - const config = { - get: sinon.stub().returns(value), - set: sinon.stub(), - }; - - expect(() => validateEncryptionKey({ config: () => config }, logger)).not.to.throwError(); - - sinon.assert.calledWith(config.set, 'xpack.reporting.encryptionKey'); - sinon.assert.calledWithMatch(logger.warning, /Generating a random key/); - sinon.assert.calledWithMatch(logger.warning, /please set xpack.reporting.encryptionKey/); - }); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_server_host.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_server_host.ts deleted file mode 100644 index 04f998fd3e5a5..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_server_host.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { ServerFacade } from '../../../../types'; -import { validateServerHost } from '../validate_server_host'; - -const configKey = 'xpack.reporting.kibanaServer.hostname'; - -describe('Reporting: Validate server host setting', () => { - it(`should log a warning and set ${configKey} if server.host is "0"`, () => { - const getStub = sinon.stub(); - getStub.withArgs('server.host').returns('0'); - getStub.withArgs(configKey).returns(undefined); - const config = { - get: getStub, - set: sinon.stub(), - }; - - expect(() => - validateServerHost(({ config: () => config } as unknown) as ServerFacade) - ).to.throwError(); - - sinon.assert.calledWith(config.set, configKey); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts index 0fdbd858b8e3c..85d9f727d7fa7 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts @@ -6,25 +6,22 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchServiceSetup } from 'kibana/server'; -import { Logger, ServerFacade } from '../../../types'; +import { Logger } from '../../../types'; import { HeadlessChromiumDriverFactory } from '../../browsers/chromium/driver_factory'; +import { ReportingConfig } from '../../types'; import { validateBrowser } from './validate_browser'; -import { validateEncryptionKey } from './validate_encryption_key'; import { validateMaxContentLength } from './validate_max_content_length'; -import { validateServerHost } from './validate_server_host'; export async function runValidations( - server: ServerFacade, + config: ReportingConfig, elasticsearch: ElasticsearchServiceSetup, browserFactory: HeadlessChromiumDriverFactory, logger: Logger ) { try { await Promise.all([ - validateBrowser(server, browserFactory, logger), - validateEncryptionKey(server, logger), - validateMaxContentLength(server, elasticsearch, logger), - validateServerHost(server), + validateBrowser(browserFactory, logger), + validateMaxContentLength(config, elasticsearch, logger), ]); logger.debug( i18n.translate('xpack.reporting.selfCheck.ok', { diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_browser.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_browser.ts index 89c49123e85bf..d6512d5eb718b 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_browser.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_browser.ts @@ -3,9 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { Browser } from 'puppeteer'; import { BROWSER_TYPE } from '../../../common/constants'; -import { ServerFacade, Logger } from '../../../types'; +import { Logger } from '../../../types'; import { HeadlessChromiumDriverFactory } from '../../browsers/chromium/driver_factory'; /* @@ -13,7 +14,6 @@ import { HeadlessChromiumDriverFactory } from '../../browsers/chromium/driver_fa * to the locally running Kibana instance. */ export const validateBrowser = async ( - server: ServerFacade, browserFactory: HeadlessChromiumDriverFactory, logger: Logger ) => { diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_encryption_key.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_encryption_key.ts deleted file mode 100644 index e0af94cbdc29c..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_encryption_key.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import crypto from 'crypto'; -import { ServerFacade, Logger } from '../../../types'; - -export function validateEncryptionKey(serverFacade: ServerFacade, logger: Logger) { - const config = serverFacade.config(); - - const encryptionKey = config.get('xpack.reporting.encryptionKey'); - if (encryptionKey == null) { - // TODO this should simply throw an error and let the handler conver it to a warning mesasge. See validateServerHost. - logger.warning( - i18n.translate('xpack.reporting.selfCheckEncryptionKey.warning', { - defaultMessage: - `Generating a random key for {setting}. To prevent pending reports ` + - `from failing on restart, please set {setting} in kibana.yml`, - values: { - setting: 'xpack.reporting.encryptionKey', - }, - }) - ); - - // @ts-ignore: No set() method on KibanaConfig, just get() and has() - config.set('xpack.reporting.encryptionKey', crypto.randomBytes(16).toString('hex')); // update config in memory to contain a usable encryption key - } -} diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js index 942dcaf842696..2551fd48b91f3 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js @@ -32,11 +32,7 @@ describe('Reporting: Validate Max Content Length', () => { }); it('should log warning messages when reporting has a higher max-size than elasticsearch', async () => { - const server = { - config: () => ({ - get: sinon.stub().returns(FIVE_HUNDRED_MEGABYTES), - }), - }; + const config = { get: sinon.stub().returns(FIVE_HUNDRED_MEGABYTES) }; const elasticsearch = { dataClient: { callAsInternalUser: () => ({ @@ -49,7 +45,7 @@ describe('Reporting: Validate Max Content Length', () => { }, }; - await validateMaxContentLength(server, elasticsearch, logger); + await validateMaxContentLength(config, elasticsearch, logger); sinon.assert.calledWithMatch( logger.warning, @@ -70,14 +66,10 @@ describe('Reporting: Validate Max Content Length', () => { }); it('should do nothing when reporting has the same max-size as elasticsearch', async () => { - const server = { - config: () => ({ - get: sinon.stub().returns(ONE_HUNDRED_MEGABYTES), - }), - }; + const config = { get: sinon.stub().returns(ONE_HUNDRED_MEGABYTES) }; expect( - async () => await validateMaxContentLength(server, elasticsearch, logger.warning) + async () => await validateMaxContentLength(config, elasticsearch, logger.warning) ).not.toThrow(); sinon.assert.notCalled(logger.warning); }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts index ce4a5b93e7431..a20905ba093d4 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts @@ -7,17 +7,17 @@ import numeral from '@elastic/numeral'; import { ElasticsearchServiceSetup } from 'kibana/server'; import { defaults, get } from 'lodash'; -import { Logger, ServerFacade } from '../../../types'; +import { Logger } from '../../../types'; +import { ReportingConfig } from '../../types'; -const KIBANA_MAX_SIZE_BYTES_PATH = 'xpack.reporting.csv.maxSizeBytes'; +const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes'; const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length'; export async function validateMaxContentLength( - server: ServerFacade, + config: ReportingConfig, elasticsearch: ElasticsearchServiceSetup, logger: Logger ) { - const config = server.config(); const { callAsInternalUser } = elasticsearch.dataClient; const elasticClusterSettingsResponse = await callAsInternalUser('cluster.getSettings', { @@ -28,13 +28,13 @@ export async function validateMaxContentLength( const elasticSearchMaxContent = get(elasticClusterSettings, 'http.max_content_length', '100mb'); const elasticSearchMaxContentBytes = numeral().unformat(elasticSearchMaxContent.toUpperCase()); - const kibanaMaxContentBytes: number = config.get(KIBANA_MAX_SIZE_BYTES_PATH); + const kibanaMaxContentBytes = config.get('csv', 'maxSizeBytes'); if (kibanaMaxContentBytes > elasticSearchMaxContentBytes) { // TODO this should simply throw an error and let the handler conver it to a warning mesasge. See validateServerHost. logger.warning( - `${KIBANA_MAX_SIZE_BYTES_PATH} (${kibanaMaxContentBytes}) is higher than ElasticSearch's ${ES_MAX_SIZE_BYTES_PATH} (${elasticSearchMaxContentBytes}). ` + - `Please set ${ES_MAX_SIZE_BYTES_PATH} in ElasticSearch to match, or lower your ${KIBANA_MAX_SIZE_BYTES_PATH} in Kibana to avoid this warning.` + `xpack.reporting.${KIBANA_MAX_SIZE_BYTES_PATH} (${kibanaMaxContentBytes}) is higher than ElasticSearch's ${ES_MAX_SIZE_BYTES_PATH} (${elasticSearchMaxContentBytes}). ` + + `Please set ${ES_MAX_SIZE_BYTES_PATH} in ElasticSearch to match, or lower your xpack.reporting.${KIBANA_MAX_SIZE_BYTES_PATH} in Kibana to avoid this warning.` ); } } diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_server_host.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_server_host.ts deleted file mode 100644 index f4f4d61246b6a..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_server_host.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ServerFacade } from '../../../types'; - -const configKey = 'xpack.reporting.kibanaServer.hostname'; - -export function validateServerHost(serverFacade: ServerFacade) { - const config = serverFacade.config(); - - const serverHost = config.get('server.host'); - const reportingKibanaHostName = config.get(configKey); - - if (!reportingKibanaHostName && serverHost === '0') { - // @ts-ignore: No set() method on KibanaConfig, just get() and has() - config.set(configKey, '0.0.0.0'); // update config in memory to allow Reporting to work - - throw new Error( - `Found 'server.host: "0"' in settings. This is incompatible with Reporting. ` + - `To enable Reporting to work, '${configKey}: 0.0.0.0' is being automatically to the configuration. ` + - `You can change to 'server.host: 0.0.0.0' or add '${configKey}: 0.0.0.0' in kibana.yml to prevent this message.` - ); - } -} diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts index 4f24cc16b2277..c9ed2e81c6792 100644 --- a/x-pack/legacy/plugins/reporting/server/plugin.ts +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -7,7 +7,7 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; import { logConfiguration } from '../log_configuration'; import { createBrowserDriverFactory } from './browsers'; -import { ReportingCore } from './core'; +import { ReportingCore, ReportingConfig } from './core'; import { createQueueFactory, enqueueJobFactory, LevelLogger, runValidations } from './lib'; import { setFieldFormats } from './services'; import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types'; @@ -17,38 +17,40 @@ import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; export class ReportingPlugin implements Plugin { + private config: ReportingConfig; private logger: LevelLogger; private reportingCore: ReportingCore; - constructor(context: PluginInitializerContext) { + constructor(context: PluginInitializerContext, config: ReportingConfig) { + this.config = config; this.logger = new LevelLogger(context.logger.get('reporting')); - this.reportingCore = new ReportingCore(this.logger); + this.reportingCore = new ReportingCore(this.logger, this.config); } public async setup(core: CoreSetup, plugins: ReportingSetupDeps) { - const { elasticsearch, usageCollection, __LEGACY } = plugins; + const { config } = this; + const { elasticsearch, __LEGACY } = plugins; - const browserDriverFactory = await createBrowserDriverFactory(__LEGACY, this.logger); // required for validations :( - runValidations(__LEGACY, elasticsearch, browserDriverFactory, this.logger); // this must run early, as it sets up config defaults + const browserDriverFactory = await createBrowserDriverFactory(config, this.logger); // required for validations :( + runValidations(config, elasticsearch, browserDriverFactory, this.logger); const { xpack_main: xpackMainLegacy, reporting: reportingLegacy } = __LEGACY.plugins; this.reportingCore.legacySetup(xpackMainLegacy, reportingLegacy, __LEGACY, plugins); // Register a function with server to manage the collection of usage stats - registerReportingUsageCollector(this.reportingCore, __LEGACY, usageCollection); + registerReportingUsageCollector(this.reportingCore, plugins); // regsister setup internals - this.reportingCore.pluginSetup({ browserDriverFactory }); + this.reportingCore.pluginSetup({ browserDriverFactory, elasticsearch }); return {}; } public async start(core: CoreStart, plugins: ReportingStartDeps) { const { reportingCore, logger } = this; - const { elasticsearch, __LEGACY } = plugins; - const esqueue = await createQueueFactory(reportingCore, __LEGACY, elasticsearch, logger); - const enqueueJob = enqueueJobFactory(reportingCore, __LEGACY, elasticsearch, logger); + const esqueue = await createQueueFactory(reportingCore, logger); + const enqueueJob = enqueueJobFactory(reportingCore, logger); this.reportingCore.pluginStart({ savedObjects: core.savedObjects, @@ -58,7 +60,8 @@ export class ReportingPlugin }); setFieldFormats(plugins.data.fieldFormats); - logConfiguration(__LEGACY, this.logger); + + logConfiguration(this.config.get('capture'), this.logger); return {}; } diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts index 56622617586f7..6b4f5dbd9203a 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -10,7 +10,7 @@ import { Legacy } from 'kibana'; import rison from 'rison-node'; import { API_BASE_URL } from '../../common/constants'; import { Logger, ReportingResponseToolkit, ServerFacade } from '../../types'; -import { ReportingSetupDeps } from '../types'; +import { ReportingCore, ReportingSetupDeps } from '../types'; import { makeRequestFacade } from './lib/make_request_facade'; import { GetRouteConfigFactoryFn, @@ -22,15 +22,17 @@ import { HandlerErrorFunction, HandlerFunction } from './types'; const BASE_GENERATE = `${API_BASE_URL}/generate`; export function registerGenerateFromJobParams( + reporting: ReportingCore, server: ServerFacade, plugins: ReportingSetupDeps, handler: HandlerFunction, handleError: HandlerErrorFunction, logger: Logger ) { + const config = reporting.getConfig(); const getRouteConfig = () => { const getOriginalRouteConfig: GetRouteConfigFactoryFn = getRouteConfigFactoryReportingPre( - server, + config, plugins, logger ); diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts index 415b6b7d64366..830953d532243 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts @@ -9,7 +9,7 @@ import { get } from 'lodash'; import { API_BASE_GENERATE_V1, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../common/constants'; import { getJobParamsFromRequest } from '../../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; import { Logger, ReportingResponseToolkit, ServerFacade } from '../../types'; -import { ReportingSetupDeps } from '../types'; +import { ReportingCore, ReportingSetupDeps } from '../types'; import { makeRequestFacade } from './lib/make_request_facade'; import { getRouteOptionsCsv } from './lib/route_config_factories'; import { HandlerErrorFunction, HandlerFunction, QueuedJobPayload } from './types'; @@ -24,13 +24,15 @@ import { HandlerErrorFunction, HandlerFunction, QueuedJobPayload } from './types * - local (transient) changes the user made to the saved object */ export function registerGenerateCsvFromSavedObject( + reporting: ReportingCore, server: ServerFacade, plugins: ReportingSetupDeps, handleRoute: HandlerFunction, handleRouteError: HandlerErrorFunction, logger: Logger ) { - const routeOptions = getRouteOptionsCsv(server, plugins, logger); + const config = reporting.getConfig(); + const routeOptions = getRouteOptionsCsv(config, plugins, logger); server.route({ path: `${API_BASE_GENERATE_V1}/csv/saved-object/{savedObjectType}:{savedObjectId}`, diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 5d17fa2e82b8c..519e49f56c377 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -16,7 +16,7 @@ import { ResponseFacade, ServerFacade, } from '../../types'; -import { ReportingSetupDeps, ReportingCore } from '../types'; +import { ReportingCore, ReportingSetupDeps } from '../types'; import { makeRequestFacade } from './lib/make_request_facade'; import { getRouteOptionsCsv } from './lib/route_config_factories'; @@ -35,8 +35,8 @@ export function registerGenerateCsvFromSavedObjectImmediate( plugins: ReportingSetupDeps, parentLogger: Logger ) { - const routeOptions = getRouteOptionsCsv(server, plugins, parentLogger); - const { elasticsearch } = plugins; + const config = reporting.getConfig(); + const routeOptions = getRouteOptionsCsv(config, plugins, parentLogger); /* * CSV export with the `immediate` option does not queue a job with Reporting's ESQueue to run the job async. Instead, this does: @@ -51,15 +51,8 @@ export function registerGenerateCsvFromSavedObjectImmediate( const request = makeRequestFacade(legacyRequest); const logger = parentLogger.clone(['savedobject-csv']); const jobParams = getJobParamsFromRequest(request, { isImmediate: true }); - - /* TODO these functions should be made available in the export types registry: - * - * const { createJobFn, executeJobFn } = exportTypesRegistry.getById(CSV_FROM_SAVEDOBJECT_JOB_TYPE) - * - * Calling an execute job factory requires passing a browserDriverFactory option, so we should not call the factory from here - */ - const createJobFn = createJobFactory(reporting, server, elasticsearch, logger); - const executeJobFn = await executeJobFactory(reporting, server, elasticsearch, logger); + const createJobFn = createJobFactory(reporting, logger); + const executeJobFn = await executeJobFactory(reporting, logger); // FIXME: does not "need" to be async const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn( jobParams, request.headers, diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts index 54d9671692c5d..8e54feac3c8a6 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts @@ -7,7 +7,7 @@ import Hapi from 'hapi'; import { createMockReportingCore } from '../../test_helpers'; import { Logger, ServerFacade } from '../../types'; -import { ReportingCore, ReportingSetupDeps } from '../../server/types'; +import { ReportingConfig, ReportingCore, ReportingSetupDeps } from '../types'; jest.mock('./lib/authorized_user_pre_routing', () => ({ authorizedUserPreRoutingFactory: () => () => ({}), @@ -22,6 +22,8 @@ import { registerJobGenerationRoutes } from './generation'; let mockServer: Hapi.Server; let mockReportingPlugin: ReportingCore; +let mockReportingConfig: ReportingConfig; + const mockLogger = ({ error: jest.fn(), debug: jest.fn(), @@ -33,8 +35,9 @@ beforeEach(async () => { port: 8080, routes: { log: { collect: true } }, }); - mockServer.config = () => ({ get: jest.fn(), has: jest.fn() }); - mockReportingPlugin = await createMockReportingCore(); + + mockReportingConfig = { get: jest.fn(), kbnConfig: { get: jest.fn() } }; + mockReportingPlugin = await createMockReportingCore(mockReportingConfig); mockReportingPlugin.getEnqueueJob = async () => jest.fn().mockImplementation(() => ({ toJSON: () => '{ "job": "data" }' })); }); diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.ts index 096ba84b63d1a..1c6129313db4b 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.ts @@ -9,7 +9,7 @@ import { errors as elasticsearchErrors } from 'elasticsearch'; import { Legacy } from 'kibana'; import { API_BASE_URL } from '../../common/constants'; import { Logger, ReportingResponseToolkit, ServerFacade } from '../../types'; -import { ReportingSetupDeps, ReportingCore } from '../types'; +import { ReportingCore, ReportingSetupDeps } from '../types'; import { registerGenerateFromJobParams } from './generate_from_jobparams'; import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject'; import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; @@ -23,8 +23,9 @@ export function registerJobGenerationRoutes( plugins: ReportingSetupDeps, logger: Logger ) { - const config = server.config(); - const DOWNLOAD_BASE_URL = config.get('server.basePath') + `${API_BASE_URL}/jobs/download`; + const config = reporting.getConfig(); + const downloadBaseUrl = + config.kbnConfig.get('server', 'basePath') + `${API_BASE_URL}/jobs/download`; /* * Generates enqueued job details to use in responses @@ -47,7 +48,7 @@ export function registerJobGenerationRoutes( return h .response({ - path: `${DOWNLOAD_BASE_URL}/${jobJson.id}`, + path: `${downloadBaseUrl}/${jobJson.id}`, job: jobJson, }) .type('application/json'); @@ -66,11 +67,11 @@ export function registerJobGenerationRoutes( return err; } - registerGenerateFromJobParams(server, plugins, handler, handleError, logger); + registerGenerateFromJobParams(reporting, server, plugins, handler, handleError, logger); // Register beta panel-action download-related API's - if (config.get('xpack.reporting.csv.enablePanelActionDownload')) { - registerGenerateCsvFromSavedObject(server, plugins, handler, handleError, logger); + if (config.get('csv', 'enablePanelActionDownload')) { + registerGenerateCsvFromSavedObject(reporting, server, plugins, handler, handleError, logger); registerGenerateCsvFromSavedObjectImmediate(reporting, server, plugins, logger); } } diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js index 071b401d2321b..9f0de844df369 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js @@ -5,7 +5,6 @@ */ import Hapi from 'hapi'; -import { memoize } from 'lodash'; import { createMockReportingCore } from '../../test_helpers'; import { ExportTypesRegistry } from '../lib/export_types_registry'; @@ -23,6 +22,7 @@ import { registerJobInfoRoutes } from './jobs'; let mockServer; let exportTypesRegistry; let mockReportingPlugin; +let mockReportingConfig; const mockLogger = { error: jest.fn(), debug: jest.fn(), @@ -30,7 +30,6 @@ const mockLogger = { beforeEach(async () => { mockServer = new Hapi.Server({ debug: false, port: 8080, routes: { log: { collect: true } } }); - mockServer.config = memoize(() => ({ get: jest.fn() })); exportTypesRegistry = new ExportTypesRegistry(); exportTypesRegistry.register({ id: 'unencoded', @@ -43,7 +42,9 @@ beforeEach(async () => { jobContentEncoding: 'base64', jobContentExtension: 'pdf', }); - mockReportingPlugin = await createMockReportingCore(); + + mockReportingConfig = { get: jest.fn(), kbnConfig: { get: jest.fn() } }; + mockReportingPlugin = await createMockReportingCore(mockReportingConfig); mockReportingPlugin.getExportTypesRegistry = () => exportTypesRegistry; }); diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts index b9aa75e0ddd00..f6f98b2377db6 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts @@ -17,7 +17,7 @@ import { ServerFacade, } from '../../types'; import { jobsQueryFactory } from '../lib/jobs_query'; -import { ReportingSetupDeps, ReportingCore } from '../types'; +import { ReportingCore, ReportingSetupDeps } from '../types'; import { deleteJobResponseHandlerFactory, downloadJobResponseHandlerFactory, @@ -41,9 +41,10 @@ export function registerJobInfoRoutes( plugins: ReportingSetupDeps, logger: Logger ) { + const config = reporting.getConfig(); const { elasticsearch } = plugins; - const jobsQuery = jobsQueryFactory(server, elasticsearch); - const getRouteConfig = getRouteConfigFactoryManagementPre(server, plugins, logger); + const jobsQuery = jobsQueryFactory(config, elasticsearch); + const getRouteConfig = getRouteConfigFactoryManagementPre(config, plugins, logger); // list jobs in the queue, paginated server.route({ @@ -141,8 +142,8 @@ export function registerJobInfoRoutes( // trigger a download of the output from a job const exportTypesRegistry = reporting.getExportTypesRegistry(); - const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(server, plugins, logger); - const downloadResponseHandler = downloadJobResponseHandlerFactory(server, elasticsearch, exportTypesRegistry); // prettier-ignore + const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(config, plugins, logger); + const downloadResponseHandler = downloadJobResponseHandlerFactory(config, elasticsearch, exportTypesRegistry); // prettier-ignore server.route({ path: `${MAIN_ENTRY}/download/{docId}`, method: 'GET', @@ -181,8 +182,8 @@ export function registerJobInfoRoutes( }); // allow a report to be deleted - const getRouteConfigDelete = getRouteConfigFactoryDeletePre(server, plugins, logger); - const deleteResponseHandler = deleteJobResponseHandlerFactory(server, elasticsearch); + const getRouteConfigDelete = getRouteConfigFactoryDeletePre(config, plugins, logger); + const deleteResponseHandler = deleteJobResponseHandlerFactory(config, elasticsearch); server.route({ path: `${MAIN_ENTRY}/delete/{docId}`, method: 'DELETE', diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.js b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.js index 3460d22592e3d..b5d6ae59ce5dd 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.js +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.js @@ -7,56 +7,48 @@ import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; describe('authorized_user_pre_routing', function() { - // the getClientShield is using `once` which forces us to use a constant mock - // which makes testing anything that is dependent on `oncePerServer` confusing. - // so createMockServer reuses the same 'instance' of the server and overwrites - // the properties to contain different values - const createMockServer = (function() { - const getUserStub = jest.fn(); - let mockConfig; - - const mockServer = { - expose() {}, - config() { - return { - get(key) { - return mockConfig[key]; - }, - }; - }, - log: function() {}, - plugins: { - xpack_main: {}, - security: { getUser: getUserStub }, - }, + const createMockConfig = (mockConfig = {}) => { + return { + get: (...keys) => mockConfig[keys.join('.')], + kbnConfig: { get: (...keys) => mockConfig[keys.join('.')] }, }; + }; + const createMockPlugins = (function() { + const getUserStub = jest.fn(); return function({ securityEnabled = true, xpackInfoUndefined = false, xpackInfoAvailable = true, + getCurrentUser = undefined, user = undefined, - config = {}, }) { - mockConfig = config; - - mockServer.plugins.xpack_main = { - info: !xpackInfoUndefined && { - isAvailable: () => xpackInfoAvailable, - feature(featureName) { - if (featureName === 'security') { - return { - isEnabled: () => securityEnabled, - isAvailable: () => xpackInfoAvailable, - }; + getUserStub.mockReset(); + getUserStub.mockResolvedValue(user); + return { + security: securityEnabled + ? { + authc: { getCurrentUser }, } + : null, + __LEGACY: { + plugins: { + xpack_main: { + info: !xpackInfoUndefined && { + isAvailable: () => xpackInfoAvailable, + feature(featureName) { + if (featureName === 'security') { + return { + isEnabled: () => securityEnabled, + isAvailable: () => xpackInfoAvailable, + }; + } + }, + }, + }, }, }, }; - - getUserStub.mockReset(); - getUserStub.mockResolvedValue(user); - return mockServer; }; })(); @@ -75,10 +67,6 @@ describe('authorized_user_pre_routing', function() { raw: { req: mockRequestRaw }, }); - const getMockPlugins = pluginSet => { - return pluginSet || { security: null }; - }; - const getMockLogger = () => ({ warn: jest.fn(), error: msg => { @@ -87,11 +75,9 @@ describe('authorized_user_pre_routing', function() { }); it('should return with boom notFound when xpackInfo is undefined', async function() { - const mockServer = createMockServer({ xpackInfoUndefined: true }); - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockServer, - getMockPlugins(), + createMockConfig(), + createMockPlugins({ xpackInfoUndefined: true }), getMockLogger() ); const response = await authorizedUserPreRouting(getMockRequest()); @@ -100,11 +86,9 @@ describe('authorized_user_pre_routing', function() { }); it(`should return with boom notFound when xpackInfo isn't available`, async function() { - const mockServer = createMockServer({ xpackInfoAvailable: false }); - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockServer, - getMockPlugins(), + createMockConfig(), + createMockPlugins({ xpackInfoAvailable: false }), getMockLogger() ); const response = await authorizedUserPreRouting(getMockRequest()); @@ -113,11 +97,9 @@ describe('authorized_user_pre_routing', function() { }); it('should return with null user when security is disabled in Elasticsearch', async function() { - const mockServer = createMockServer({ securityEnabled: false }); - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockServer, - getMockPlugins(), + createMockConfig(), + createMockPlugins({ securityEnabled: false }), getMockLogger() ); const response = await authorizedUserPreRouting(getMockRequest()); @@ -125,16 +107,14 @@ describe('authorized_user_pre_routing', function() { }); it('should return with boom unauthenticated when security is enabled but no authenticated user', async function() { - const mockServer = createMockServer({ + const mockPlugins = createMockPlugins({ user: null, config: { 'xpack.reporting.roles.allow': ['.reporting_user'] }, }); - const mockPlugins = getMockPlugins({ - security: { authc: { getCurrentUser: () => null } }, - }); + mockPlugins.security = { authc: { getCurrentUser: () => null } }; const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockServer, + createMockConfig(), mockPlugins, getMockLogger() ); @@ -144,16 +124,14 @@ describe('authorized_user_pre_routing', function() { }); it(`should return with boom forbidden when security is enabled but user doesn't have allowed role`, async function() { - const mockServer = createMockServer({ + const mockConfig = createMockConfig({ 'roles.allow': ['.reporting_user'] }); + const mockPlugins = createMockPlugins({ user: { roles: [] }, - config: { 'xpack.reporting.roles.allow': ['.reporting_user'] }, - }); - const mockPlugins = getMockPlugins({ - security: { authc: { getCurrentUser: () => ({ roles: ['something_else'] }) } }, + getCurrentUser: () => ({ roles: ['something_else'] }), }); const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockServer, + mockConfig, mockPlugins, getMockLogger() ); @@ -164,18 +142,14 @@ describe('authorized_user_pre_routing', function() { it('should return with user when security is enabled and user has explicitly allowed role', async function() { const user = { roles: ['.reporting_user', 'something_else'] }; - const mockServer = createMockServer({ + const mockConfig = createMockConfig({ 'roles.allow': ['.reporting_user'] }); + const mockPlugins = createMockPlugins({ user, - config: { 'xpack.reporting.roles.allow': ['.reporting_user'] }, - }); - const mockPlugins = getMockPlugins({ - security: { - authc: { getCurrentUser: () => ({ roles: ['.reporting_user', 'something_else'] }) }, - }, + getCurrentUser: () => ({ roles: ['.reporting_user', 'something_else'] }), }); const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockServer, + mockConfig, mockPlugins, getMockLogger() ); @@ -185,16 +159,13 @@ describe('authorized_user_pre_routing', function() { it('should return with user when security is enabled and user has superuser role', async function() { const user = { roles: ['superuser', 'something_else'] }; - const mockServer = createMockServer({ - user, - config: { 'xpack.reporting.roles.allow': [] }, - }); - const mockPlugins = getMockPlugins({ - security: { authc: { getCurrentUser: () => ({ roles: ['superuser', 'something_else'] }) } }, + const mockConfig = createMockConfig({ 'roles.allow': [] }); + const mockPlugins = createMockPlugins({ + getCurrentUser: () => ({ roles: ['superuser', 'something_else'] }), }); const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockServer, + mockConfig, mockPlugins, getMockLogger() ); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts index c5f8c78016f61..1ca28ca62a7f2 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts @@ -7,7 +7,8 @@ import Boom from 'boom'; import { Legacy } from 'kibana'; import { AuthenticatedUser } from '../../../../../../plugins/security/server'; -import { Logger, ServerFacade } from '../../../types'; +import { ReportingConfig } from '../../../server'; +import { Logger } from '../../../types'; import { getUserFactory } from '../../lib/get_user'; import { ReportingSetupDeps } from '../../types'; @@ -18,16 +19,14 @@ export type PreRoutingFunction = ( ) => Promise | AuthenticatedUser | null>; export const authorizedUserPreRoutingFactory = function authorizedUserPreRoutingFn( - server: ServerFacade, + config: ReportingConfig, plugins: ReportingSetupDeps, logger: Logger ) { - const getUser = getUserFactory(server, plugins.security); - const config = server.config(); + const getUser = getUserFactory(plugins.security, logger); + const { info: xpackInfo } = plugins.__LEGACY.plugins.xpack_main; return async function authorizedUserPreRouting(request: Legacy.Request) { - const xpackInfo = server.plugins.xpack_main.info; - if (!xpackInfo || !xpackInfo.isAvailable()) { logger.warn('Unable to authorize user before xpack info is available.', [ 'authorizedUserPreRouting', @@ -46,10 +45,7 @@ export const authorizedUserPreRoutingFactory = function authorizedUserPreRouting return Boom.unauthorized(`Sorry, you aren't authenticated`); } - const authorizedRoles = [ - superuserRole, - ...(config.get('xpack.reporting.roles.allow') as string[]), - ]; + const authorizedRoles = [superuserRole, ...(config.get('roles', 'allow') as string[])]; if (!user.roles.find(role => authorizedRoles.includes(role))) { return Boom.forbidden(`Sorry, you don't have access to Reporting`); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts index fb3944ea33552..aef37754681ec 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -8,13 +8,7 @@ import contentDisposition from 'content-disposition'; import * as _ from 'lodash'; import { CSV_JOB_TYPE } from '../../../common/constants'; -import { - ExportTypeDefinition, - ExportTypesRegistry, - JobDocOutput, - JobSource, - ServerFacade, -} from '../../../types'; +import { ExportTypeDefinition, ExportTypesRegistry, JobDocOutput, JobSource } from '../../../types'; interface ICustomHeaders { [x: string]: any; @@ -22,9 +16,15 @@ interface ICustomHeaders { type ExportTypeType = ExportTypeDefinition; +interface ErrorFromPayload { + message: string; + reason: string | null; +} + +// A camelCase version of JobDocOutput interface Payload { statusCode: number; - content: any; + content: string | Buffer | ErrorFromPayload; contentType: string; headers: Record; } @@ -48,20 +48,17 @@ const getReportingHeaders = (output: JobDocOutput, exportType: ExportTypeType) = return metaDataHeaders; }; -export function getDocumentPayloadFactory( - server: ServerFacade, - exportTypesRegistry: ExportTypesRegistry -) { - function encodeContent(content: string | null, exportType: ExportTypeType) { +export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegistry) { + function encodeContent(content: string | null, exportType: ExportTypeType): Buffer | string { switch (exportType.jobContentEncoding) { case 'base64': - return content ? Buffer.from(content, 'base64') : content; // Buffer.from rejects null + return content ? Buffer.from(content, 'base64') : ''; // convert null to empty string default: - return content; + return content ? content : ''; // convert null to empty string } } - function getCompleted(output: JobDocOutput, jobType: string, title: string) { + function getCompleted(output: JobDocOutput, jobType: string, title: string): Payload { const exportType = exportTypesRegistry.get((item: ExportTypeType) => item.jobType === jobType); const filename = getTitle(exportType, title); const headers = getReportingHeaders(output, exportType); @@ -77,7 +74,7 @@ export function getDocumentPayloadFactory( }; } - function getFailure(output: JobDocOutput) { + function getFailure(output: JobDocOutput): Payload { return { statusCode: 500, content: { diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts index 30627d5b23230..e7e7c866db96a 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -5,11 +5,12 @@ */ import Boom from 'boom'; -import { ElasticsearchServiceSetup } from 'kibana/server'; import { ResponseToolkit } from 'hapi'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants'; -import { ExportTypesRegistry, ServerFacade } from '../../../types'; +import { ExportTypesRegistry } from '../../../types'; import { jobsQueryFactory } from '../../lib/jobs_query'; +import { ReportingConfig } from '../../types'; import { getDocumentPayloadFactory } from './get_document_payload'; interface JobResponseHandlerParams { @@ -21,12 +22,12 @@ interface JobResponseHandlerOpts { } export function downloadJobResponseHandlerFactory( - server: ServerFacade, + config: ReportingConfig, elasticsearch: ElasticsearchServiceSetup, exportTypesRegistry: ExportTypesRegistry ) { - const jobsQuery = jobsQueryFactory(server, elasticsearch); - const getDocumentPayload = getDocumentPayloadFactory(server, exportTypesRegistry); + const jobsQuery = jobsQueryFactory(config, elasticsearch); + const getDocumentPayload = getDocumentPayloadFactory(exportTypesRegistry); return function jobResponseHandler( validJobTypes: string[], @@ -70,10 +71,10 @@ export function downloadJobResponseHandlerFactory( } export function deleteJobResponseHandlerFactory( - server: ServerFacade, + config: ReportingConfig, elasticsearch: ElasticsearchServiceSetup ) { - const jobsQuery = jobsQueryFactory(server, elasticsearch); + const jobsQuery = jobsQueryFactory(config, elasticsearch); return async function deleteJobResponseHander( validJobTypes: string[], diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.ts index 9e618ff1fe40a..8a79566aafae2 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.ts @@ -6,17 +6,17 @@ import Boom from 'boom'; import { Legacy } from 'kibana'; -import { Logger, ServerFacade } from '../../../types'; -import { ReportingSetupDeps } from '../../types'; +import { Logger } from '../../../types'; +import { ReportingConfig, ReportingSetupDeps } from '../../types'; export type GetReportingFeatureIdFn = (request: Legacy.Request) => string; export const reportingFeaturePreRoutingFactory = function reportingFeaturePreRoutingFn( - server: ServerFacade, + config: ReportingConfig, plugins: ReportingSetupDeps, logger: Logger ) { - const xpackMainPlugin = server.plugins.xpack_main; + const xpackMainPlugin = plugins.__LEGACY.plugins.xpack_main; const pluginId = 'reporting'; // License checking and enable/disable logic diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts index 3d275d34e2f7d..06f7efaa9dcbb 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts @@ -6,8 +6,8 @@ import Joi from 'joi'; import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; -import { Logger, ServerFacade } from '../../../types'; -import { ReportingSetupDeps } from '../../types'; +import { Logger } from '../../../types'; +import { ReportingConfig, ReportingSetupDeps } from '../../types'; import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; import { GetReportingFeatureIdFn, @@ -29,12 +29,12 @@ export type GetRouteConfigFactoryFn = ( ) => RouteConfigFactory; export function getRouteConfigFactoryReportingPre( - server: ServerFacade, + config: ReportingConfig, plugins: ReportingSetupDeps, logger: Logger ): GetRouteConfigFactoryFn { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(server, plugins, logger); - const reportingFeaturePreRouting = reportingFeaturePreRoutingFactory(server, plugins, logger); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(config, plugins, logger); + const reportingFeaturePreRouting = reportingFeaturePreRoutingFactory(config, plugins, logger); return (getFeatureId?: GetReportingFeatureIdFn): RouteConfigFactory => { const preRouting: any[] = [{ method: authorizedUserPreRouting, assign: 'user' }]; @@ -50,11 +50,11 @@ export function getRouteConfigFactoryReportingPre( } export function getRouteOptionsCsv( - server: ServerFacade, + config: ReportingConfig, plugins: ReportingSetupDeps, logger: Logger ) { - const getRouteConfig = getRouteConfigFactoryReportingPre(server, plugins, logger); + const getRouteConfig = getRouteConfigFactoryReportingPre(config, plugins, logger); return { ...getRouteConfig(() => CSV_FROM_SAVEDOBJECT_JOB_TYPE), validate: { @@ -75,12 +75,12 @@ export function getRouteOptionsCsv( } export function getRouteConfigFactoryManagementPre( - server: ServerFacade, + config: ReportingConfig, plugins: ReportingSetupDeps, logger: Logger ): GetRouteConfigFactoryFn { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(server, plugins, logger); - const reportingFeaturePreRouting = reportingFeaturePreRoutingFactory(server, plugins, logger); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(config, plugins, logger); + const reportingFeaturePreRouting = reportingFeaturePreRoutingFactory(config, plugins, logger); const managementPreRouting = reportingFeaturePreRouting(() => 'management'); return (): RouteConfigFactory => { @@ -99,11 +99,11 @@ export function getRouteConfigFactoryManagementPre( // Additionally, the range-request doesn't alleviate any performance issues on the server as the entire // download is loaded into memory. export function getRouteConfigFactoryDownloadPre( - server: ServerFacade, + config: ReportingConfig, plugins: ReportingSetupDeps, logger: Logger ): GetRouteConfigFactoryFn { - const getManagementRouteConfig = getRouteConfigFactoryManagementPre(server, plugins, logger); + const getManagementRouteConfig = getRouteConfigFactoryManagementPre(config, plugins, logger); return (): RouteConfigFactory => ({ ...getManagementRouteConfig(), tags: [API_TAG, 'download'], @@ -114,11 +114,11 @@ export function getRouteConfigFactoryDownloadPre( } export function getRouteConfigFactoryDeletePre( - server: ServerFacade, + config: ReportingConfig, plugins: ReportingSetupDeps, logger: Logger ): GetRouteConfigFactoryFn { - const getManagementRouteConfig = getRouteConfigFactoryManagementPre(server, plugins, logger); + const getManagementRouteConfig = getRouteConfigFactoryManagementPre(config, plugins, logger); return (): RouteConfigFactory => ({ ...getManagementRouteConfig(), tags: [API_TAG, 'delete'], diff --git a/x-pack/legacy/plugins/reporting/server/types.d.ts b/x-pack/legacy/plugins/reporting/server/types.d.ts index 59b7bc2020ad9..bec00688432cc 100644 --- a/x-pack/legacy/plugins/reporting/server/types.d.ts +++ b/x-pack/legacy/plugins/reporting/server/types.d.ts @@ -11,16 +11,16 @@ import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/ import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; import { ReportingPluginSpecOptions } from '../types'; +import { ReportingConfig, ReportingConfigType } from './core'; export interface ReportingSetupDeps { elasticsearch: ElasticsearchServiceSetup; security: SecurityPluginSetup; - usageCollection: UsageCollectionSetup; + usageCollection?: UsageCollectionSetup; __LEGACY: LegacySetup; } export interface ReportingStartDeps { - elasticsearch: ElasticsearchServiceSetup; data: DataPluginStart; __LEGACY: LegacySetup; } @@ -31,9 +31,7 @@ export type ReportingStart = object; export interface LegacySetup { config: Legacy.Server['config']; - info: Legacy.Server['info']; plugins: { - elasticsearch: Legacy.Server['plugins']['elasticsearch']; xpack_main: XPackMainPlugin & { status?: any; }; @@ -42,4 +40,7 @@ export interface LegacySetup { route: Legacy.Server['route']; } -export { ReportingCore } from './core'; +export { ReportingConfig, ReportingConfigType, ReportingCore } from './core'; + +export type CaptureConfig = ReportingConfigType['capture']; +export type ScrollConfig = ReportingConfigType['csv']['scroll']; diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts index bd2d0cb835a79..e9523d9e70202 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts @@ -5,7 +5,11 @@ */ import { get } from 'lodash'; -import { ServerFacade, ExportTypesRegistry, ESCallCluster } from '../../types'; +import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; +import { ESCallCluster, ExportTypesRegistry } from '../../types'; +import { ReportingConfig } from '../types'; +import { decorateRangeStats } from './decorate_range_stats'; +import { getExportTypesHandler } from './get_export_type_handler'; import { AggregationBuckets, AggregationResults, @@ -15,8 +19,8 @@ import { RangeAggregationResults, RangeStats, } from './types'; -import { decorateRangeStats } from './decorate_range_stats'; -import { getExportTypesHandler } from './get_export_type_handler'; + +type XPackInfo = XPackMainPlugin['info']; const JOB_TYPES_KEY = 'jobTypes'; const JOB_TYPES_FIELD = 'jobtype'; @@ -79,10 +83,7 @@ type RangeStatSets = Partial< last7Days: RangeStats; } >; -async function handleResponse( - server: ServerFacade, - response: AggregationResults -): Promise { +async function handleResponse(response: AggregationResults): Promise { const buckets = get(response, 'aggregations.ranges.buckets'); if (!buckets) { return {}; @@ -101,12 +102,12 @@ async function handleResponse( } export async function getReportingUsage( - server: ServerFacade, + config: ReportingConfig, + xpackMainInfo: XPackInfo, callCluster: ESCallCluster, exportTypesRegistry: ExportTypesRegistry ) { - const config = server.config(); - const reportingIndex = config.get('xpack.reporting.index'); + const reportingIndex = config.get('index'); const params = { index: `${reportingIndex}-*`, @@ -140,15 +141,16 @@ export async function getReportingUsage( }; return callCluster('search', params) - .then((response: AggregationResults) => handleResponse(server, response)) + .then((response: AggregationResults) => handleResponse(response)) .then((usage: RangeStatSets) => { // Allow this to explicitly throw an exception if/when this config is deprecated, // because we shouldn't collect browserType in that case! - const browserType = config.get('xpack.reporting.capture.browser.type'); + const browserType = config.get('capture', 'browser', 'type'); - const xpackInfo = server.plugins.xpack_main.info; const exportTypesHandler = getExportTypesHandler(exportTypesRegistry); - const availability = exportTypesHandler.getAvailability(xpackInfo) as FeatureAvailabilityMap; + const availability = exportTypesHandler.getAvailability( + xpackMainInfo + ) as FeatureAvailabilityMap; const { lastDay, last7Days, ...all } = usage; diff --git a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js index a6d753f9b107a..929109e66914d 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js @@ -24,62 +24,60 @@ function getMockUsageCollection() { makeUsageCollector: options => { return new MockUsageCollector(this, options); }, + registerCollector: sinon.stub(), }; } -function getServerMock(customization) { - const getLicenseCheckResults = sinon.stub().returns({}); - const defaultServerMock = { - plugins: { - security: { - isAuthenticated: sinon.stub().returns(true), - }, - xpack_main: { - info: { - isAvailable: sinon.stub().returns(true), - feature: () => ({ - getLicenseCheckResults, - }), - license: { - isOneOf: sinon.stub().returns(false), - getType: sinon.stub().returns('platinum'), - }, - toJSON: () => ({ b: 1 }), - }, +function getPluginsMock( + { license, usageCollection = getMockUsageCollection() } = { license: 'platinum' } +) { + const mockXpackMain = { + info: { + isAvailable: sinon.stub().returns(true), + feature: () => ({ + getLicenseCheckResults: sinon.stub(), + }), + license: { + isOneOf: sinon.stub().returns(false), + getType: sinon.stub().returns(license), }, + toJSON: () => ({ b: 1 }), }, - log: () => {}, - config: () => ({ - get: key => { - if (key === 'xpack.reporting.enabled') { - return true; - } else if (key === 'xpack.reporting.index') { - return '.reporting-index'; - } + }; + return { + usageCollection, + __LEGACY: { + plugins: { + xpack_main: mockXpackMain, }, - }), + }, }; - return Object.assign(defaultServerMock, customization); } +const getMockReportingConfig = () => ({ + get: () => {}, + kbnConfig: { get: () => '' }, +}); const getResponseMock = (customization = {}) => customization; describe('license checks', () => { + let mockConfig; + beforeAll(async () => { + mockConfig = getMockReportingConfig(); + }); + describe('with a basic license', () => { let usageStats; beforeAll(async () => { - const serverWithBasicLicenseMock = getServerMock(); - serverWithBasicLicenseMock.plugins.xpack_main.info.license.getType = sinon - .stub() - .returns('basic'); + const plugins = getPluginsMock({ license: 'basic' }); const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); - const usageCollection = getMockUsageCollection(); - const { fetch: getReportingUsage } = getReportingUsageCollector( - serverWithBasicLicenseMock, - usageCollection, + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, exportTypesRegistry ); - usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry); + usageStats = await fetch(callClusterMock, exportTypesRegistry); }); test('sets enables to true', async () => { @@ -98,18 +96,15 @@ describe('license checks', () => { describe('with no license', () => { let usageStats; beforeAll(async () => { - const serverWithNoLicenseMock = getServerMock(); - serverWithNoLicenseMock.plugins.xpack_main.info.license.getType = sinon - .stub() - .returns('none'); + const plugins = getPluginsMock({ license: 'none' }); const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); - const usageCollection = getMockUsageCollection(); - const { fetch: getReportingUsage } = getReportingUsageCollector( - serverWithNoLicenseMock, - usageCollection, + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, exportTypesRegistry ); - usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry); + usageStats = await fetch(callClusterMock, exportTypesRegistry); }); test('sets enables to true', async () => { @@ -128,18 +123,15 @@ describe('license checks', () => { describe('with platinum license', () => { let usageStats; beforeAll(async () => { - const serverWithPlatinumLicenseMock = getServerMock(); - serverWithPlatinumLicenseMock.plugins.xpack_main.info.license.getType = sinon - .stub() - .returns('platinum'); + const plugins = getPluginsMock({ license: 'platinum' }); const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); - const usageCollection = getMockUsageCollection(); - const { fetch: getReportingUsage } = getReportingUsageCollector( - serverWithPlatinumLicenseMock, - usageCollection, + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, exportTypesRegistry ); - usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry); + usageStats = await fetch(callClusterMock, exportTypesRegistry); }); test('sets enables to true', async () => { @@ -158,18 +150,15 @@ describe('license checks', () => { describe('with no usage data', () => { let usageStats; beforeAll(async () => { - const serverWithBasicLicenseMock = getServerMock(); - serverWithBasicLicenseMock.plugins.xpack_main.info.license.getType = sinon - .stub() - .returns('basic'); + const plugins = getPluginsMock({ license: 'basic' }); const callClusterMock = jest.fn(() => Promise.resolve({})); - const usageCollection = getMockUsageCollection(); - const { fetch: getReportingUsage } = getReportingUsageCollector( - serverWithBasicLicenseMock, - usageCollection, + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, exportTypesRegistry ); - usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry); + usageStats = await fetch(callClusterMock, exportTypesRegistry); }); test('sets enables to true', async () => { @@ -183,21 +172,15 @@ describe('license checks', () => { }); describe('data modeling', () => { - let getReportingUsage; - beforeAll(async () => { - const usageCollection = getMockUsageCollection(); - const serverWithPlatinumLicenseMock = getServerMock(); - serverWithPlatinumLicenseMock.plugins.xpack_main.info.license.getType = sinon - .stub() - .returns('platinum'); - ({ fetch: getReportingUsage } = getReportingUsageCollector( - serverWithPlatinumLicenseMock, - usageCollection, - exportTypesRegistry - )); - }); - test('with normal looking usage data', async () => { + const mockConfig = getMockReportingConfig(); + const plugins = getPluginsMock(); + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, + exportTypesRegistry + ); const callClusterMock = jest.fn(() => Promise.resolve( getResponseMock({ @@ -320,7 +303,7 @@ describe('data modeling', () => { ) ); - const usageStats = await getReportingUsage(callClusterMock); + const usageStats = await fetch(callClusterMock); expect(usageStats).toMatchInlineSnapshot(` Object { "PNG": Object { @@ -415,20 +398,16 @@ describe('data modeling', () => { }); describe('Ready for collection observable', () => { - let mockReporting; - - beforeEach(async () => { - mockReporting = await createMockReportingCore(); - }); - test('converts observable to promise', async () => { - const serverWithBasicLicenseMock = getServerMock(); + const mockConfig = getMockReportingConfig(); + const mockReporting = await createMockReportingCore(mockConfig); + + const usageCollection = getMockUsageCollection(); const makeCollectorSpy = sinon.spy(); - const usageCollection = { - makeUsageCollector: makeCollectorSpy, - registerCollector: sinon.stub(), - }; - registerReportingUsageCollector(mockReporting, serverWithBasicLicenseMock, usageCollection); + usageCollection.makeUsageCollector = makeCollectorSpy; + + const plugins = getPluginsMock({ usageCollection }); + registerReportingUsageCollector(mockReporting, plugins); const [args] = makeCollectorSpy.firstCall.args; expect(args).toMatchInlineSnapshot(` diff --git a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts index 14202530fb6c7..8f9d65c200dad 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts @@ -5,29 +5,32 @@ */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; import { KIBANA_REPORTING_TYPE } from '../../common/constants'; -import { ReportingCore } from '../../server'; -import { ESCallCluster, ExportTypesRegistry, ServerFacade } from '../../types'; +import { ReportingConfig, ReportingCore, ReportingSetupDeps } from '../../server/types'; +import { ESCallCluster, ExportTypesRegistry } from '../../types'; import { getReportingUsage } from './get_reporting_usage'; import { RangeStats } from './types'; +type XPackInfo = XPackMainPlugin['info']; + // places the reporting data as kibana stats const METATYPE = 'kibana_stats'; /* - * @param {Object} server * @return {Object} kibana usage stats type collection object */ export function getReportingUsageCollector( - server: ServerFacade, + config: ReportingConfig, usageCollection: UsageCollectionSetup, + xpackMainInfo: XPackInfo, exportTypesRegistry: ExportTypesRegistry, isReady: () => Promise ) { return usageCollection.makeUsageCollector({ type: KIBANA_REPORTING_TYPE, fetch: (callCluster: ESCallCluster) => - getReportingUsage(server, callCluster, exportTypesRegistry), + getReportingUsage(config, xpackMainInfo, callCluster, exportTypesRegistry), isReady, /* @@ -52,17 +55,23 @@ export function getReportingUsageCollector( export function registerReportingUsageCollector( reporting: ReportingCore, - server: ServerFacade, - usageCollection: UsageCollectionSetup + plugins: ReportingSetupDeps ) { + if (!plugins.usageCollection) { + return; + } + const xpackMainInfo = plugins.__LEGACY.plugins.xpack_main.info; + const exportTypesRegistry = reporting.getExportTypesRegistry(); const collectionIsReady = reporting.pluginHasStarted.bind(reporting); + const config = reporting.getConfig(); const collector = getReportingUsageCollector( - server, - usageCollection, + config, + plugins.usageCollection, + xpackMainInfo, exportTypesRegistry, collectionIsReady ); - usageCollection.registerCollector(collector); + plugins.usageCollection.registerCollector(collector); } diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts index 883276d43e27e..930aa7601b8cb 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_browserdriverfactory.ts @@ -10,7 +10,8 @@ import * as contexts from '../export_types/common/lib/screenshots/constants'; import { ElementsPositionAndAttribute } from '../export_types/common/lib/screenshots/types'; import { HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../server/browsers'; import { createDriverFactory } from '../server/browsers/chromium'; -import { BrowserConfig, CaptureConfig, Logger } from '../types'; +import { CaptureConfig } from '../server/types'; +import { Logger } from '../types'; interface CreateMockBrowserDriverFactoryOpts { evaluate: jest.Mock, any[]>; @@ -93,24 +94,34 @@ export const createMockBrowserDriverFactory = async ( logger: Logger, opts: Partial ): Promise => { - const browserConfig = { - inspect: true, - userDataDir: '/usr/data/dir', - viewport: { width: 12, height: 12 }, - disableSandbox: false, - proxy: { enabled: false }, - } as BrowserConfig; + const captureConfig = { + timeouts: { openUrl: 30000, waitForElements: 30000, renderComplete: 30000 }, + browser: { + type: 'chromium', + chromium: { + inspect: false, + disableSandbox: false, + userDataDir: '/usr/data/dir', + viewport: { width: 12, height: 12 }, + proxy: { enabled: false, server: undefined, bypass: undefined }, + }, + autoDownload: false, + inspect: true, + userDataDir: '/usr/data/dir', + viewport: { width: 12, height: 12 }, + disableSandbox: false, + proxy: { enabled: false, server: undefined, bypass: undefined }, + maxScreenshotDimension: undefined, + }, + networkPolicy: { enabled: true, rules: [] }, + viewport: { width: 800, height: 600 }, + loadDelay: 2000, + zoom: 1, + maxAttempts: 1, + } as CaptureConfig; const binaryPath = '/usr/local/share/common/secure/'; - const captureConfig = { networkPolicy: {}, timeouts: {} } as CaptureConfig; - - const mockBrowserDriverFactory = await createDriverFactory( - binaryPath, - logger, - browserConfig, - captureConfig - ); - + const mockBrowserDriverFactory = await createDriverFactory(binaryPath, logger, captureConfig); const mockPage = {} as Page; const mockBrowserDriver = new HeadlessChromiumDriver(mockPage, { inspect: true, diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts index 0250e6c0a9afd..be60b56dcc0c1 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_layoutinstance.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createLayout } from '../export_types/common/layouts'; import { LayoutTypes } from '../export_types/common/constants'; +import { createLayout } from '../export_types/common/layouts'; import { LayoutInstance } from '../export_types/common/layouts/layout'; -import { ServerFacade } from '../types'; +import { CaptureConfig } from '../server/types'; -export const createMockLayoutInstance = (__LEGACY: ServerFacade) => { - const mockLayout = createLayout(__LEGACY, { +export const createMockLayoutInstance = (captureConfig: CaptureConfig) => { + const mockLayout = createLayout(captureConfig, { id: LayoutTypes.PRESERVE_LAYOUT, dimensions: { height: 12, width: 12 }, }) as LayoutInstance; diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts index 2cd129d47b3f9..34ff91d1972a0 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts @@ -16,24 +16,26 @@ jest.mock('../log_configuration'); import { EventEmitter } from 'events'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { coreMock } from 'src/core/server/mocks'; -import { ReportingPlugin, ReportingCore } from '../server'; +import { ReportingPlugin, ReportingCore, ReportingConfig } from '../server'; import { ReportingSetupDeps, ReportingStartDeps } from '../server/types'; -export const createMockSetupDeps = (setupMock?: any): ReportingSetupDeps => ({ - elasticsearch: setupMock.elasticsearch, - security: setupMock.security, - usageCollection: {} as any, - __LEGACY: { plugins: { xpack_main: { status: new EventEmitter() } } } as any, -}); +const createMockSetupDeps = (setupMock?: any): ReportingSetupDeps => { + return { + elasticsearch: setupMock.elasticsearch, + security: setupMock.security, + usageCollection: {} as any, + __LEGACY: { plugins: { xpack_main: { status: new EventEmitter() } } } as any, + }; +}; export const createMockStartDeps = (startMock?: any): ReportingStartDeps => ({ data: startMock.data, - elasticsearch: startMock.elasticsearch, __LEGACY: {} as any, }); -const createMockReportingPlugin = async (config = {}): Promise => { - const plugin = new ReportingPlugin(coreMock.createPluginInitializerContext(config)); +const createMockReportingPlugin = async (config: ReportingConfig): Promise => { + config = config || {}; + const plugin = new ReportingPlugin(coreMock.createPluginInitializerContext(config), config); const setupMock = coreMock.createSetup(); const coreStartMock = coreMock.createStart(); const startMock = { @@ -47,7 +49,8 @@ const createMockReportingPlugin = async (config = {}): Promise return plugin; }; -export const createMockReportingCore = async (config = {}): Promise => { +export const createMockReportingCore = async (config: ReportingConfig): Promise => { + config = config || {}; const plugin = await createMockReportingPlugin(config); return plugin.getReportingCore(); }; diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts index bb7851ba036a9..531e1dcaf84e0 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts @@ -3,36 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { memoize } from 'lodash'; -import { ServerFacade } from '../types'; - -export const createMockServer = ({ settings = {} }: any): ServerFacade => { - const mockServer = { - config: memoize(() => ({ get: jest.fn() })), - info: { - protocol: 'http', - }, - plugins: { - elasticsearch: { - getCluster: memoize(() => { - return { - callWithRequest: jest.fn(), - }; - }), - }, - }, - }; - const defaultSettings: any = { - 'xpack.reporting.encryptionKey': 'testencryptionkey', - 'server.basePath': '/sbp', - 'server.host': 'localhost', - 'server.port': 5601, - 'xpack.reporting.kibanaServer': {}, - }; - mockServer.config().get.mockImplementation((key: any) => { - return key in settings ? settings[key] : defaultSettings[key]; - }); +import { ServerFacade } from '../types'; - return (mockServer as unknown) as ServerFacade; +export const createMockServer = (): ServerFacade => { + const mockServer = {}; + return mockServer as any; }; diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index 238079ba92a29..09d53278941c9 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -7,14 +7,11 @@ import { EventEmitter } from 'events'; import { ResponseObject } from 'hapi'; import { Legacy } from 'kibana'; -import { ElasticsearchServiceSetup } from 'kibana/server'; import { CallCluster } from '../../../../src/legacy/core_plugins/elasticsearch'; import { CancellationToken } from './common/cancellation_token'; -import { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; -import { BrowserType } from './server/browsers/types'; -import { LevelLogger } from './server/lib/level_logger'; import { ReportingCore } from './server/core'; -import { LegacySetup, ReportingStartDeps, ReportingSetup, ReportingStart } from './server/types'; +import { LevelLogger } from './server/lib/level_logger'; +import { LegacySetup } from './server/types'; export type Job = EventEmitter & { id: string; @@ -25,8 +22,8 @@ export type Job = EventEmitter & { export interface NetworkPolicyRule { allow: boolean; - protocol: string; - host: string; + protocol?: string; + host?: string; } export interface NetworkPolicy { @@ -93,51 +90,6 @@ export type ReportingResponseToolkit = Legacy.ResponseToolkit; export type ESCallCluster = CallCluster; -/* - * Reporting Config - */ - -export interface CaptureConfig { - browser: { - type: BrowserType; - autoDownload: boolean; - chromium: BrowserConfig; - }; - maxAttempts: number; - networkPolicy: NetworkPolicy; - loadDelay: number; - timeouts: { - openUrl: number; - waitForElements: number; - renderComplet: number; - }; -} - -export interface BrowserConfig { - inspect: boolean; - userDataDir: string; - viewport: { width: number; height: number }; - disableSandbox: boolean; - proxy: { - enabled: boolean; - server: string; - bypass?: string[]; - }; -} - -export interface QueueConfig { - indexInterval: string; - pollEnabled: boolean; - pollInterval: number; - pollIntervalErrorMultiplier: number; - timeout: number; -} - -export interface ScrollConfig { - duration: string; - size: number; -} - export interface ElementPosition { boundingClientRect: { // modern browsers support x/y, but older ones don't @@ -274,16 +226,12 @@ export interface ESQueueInstance { export type CreateJobFactory = ( reporting: ReportingCore, - server: ServerFacade, - elasticsearch: ElasticsearchServiceSetup, logger: LevelLogger ) => CreateJobFnType; export type ExecuteJobFactory = ( reporting: ReportingCore, - server: ServerFacade, - elasticsearch: ElasticsearchServiceSetup, logger: LevelLogger -) => Promise; +) => Promise; // FIXME: does not "need" to be async export interface ExportTypeDefinition< JobParamsType, diff --git a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts index 646132c3f88eb..f38cb2285b480 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts @@ -42,16 +42,15 @@ describe('Detections', () => { cy.get(NUMBER_OF_SIGNALS) .invoke('text') .then(numberOfSignals => { - cy.get(SHOWING_SIGNALS) - .invoke('text') - .should('eql', `Showing ${numberOfSignals} signals`); + cy.get(SHOWING_SIGNALS).should('have.text', `Showing ${numberOfSignals} signals`); const numberOfSignalsToBeClosed = 3; selectNumberOfSignals(numberOfSignalsToBeClosed); - cy.get(SELECTED_SIGNALS) - .invoke('text') - .should('eql', `Selected ${numberOfSignalsToBeClosed} signals`); + cy.get(SELECTED_SIGNALS).should( + 'have.text', + `Selected ${numberOfSignalsToBeClosed} signals` + ); closeSignals(); waitForSignals(); @@ -59,30 +58,33 @@ describe('Detections', () => { waitForSignals(); const expectedNumberOfSignalsAfterClosing = +numberOfSignals - numberOfSignalsToBeClosed; - cy.get(NUMBER_OF_SIGNALS) - .invoke('text') - .should('eq', expectedNumberOfSignalsAfterClosing.toString()); - cy.get(SHOWING_SIGNALS) - .invoke('text') - .should('eql', `Showing ${expectedNumberOfSignalsAfterClosing.toString()} signals`); + cy.get(NUMBER_OF_SIGNALS).should( + 'have.text', + expectedNumberOfSignalsAfterClosing.toString() + ); + + cy.get(SHOWING_SIGNALS).should( + 'have.text', + `Showing ${expectedNumberOfSignalsAfterClosing.toString()} signals` + ); goToClosedSignals(); waitForSignals(); - cy.get(NUMBER_OF_SIGNALS) - .invoke('text') - .should('eql', numberOfSignalsToBeClosed.toString()); - cy.get(SHOWING_SIGNALS) - .invoke('text') - .should('eql', `Showing ${numberOfSignalsToBeClosed.toString()} signals`); + cy.get(NUMBER_OF_SIGNALS).should('have.text', numberOfSignalsToBeClosed.toString()); + cy.get(SHOWING_SIGNALS).should( + 'have.text', + `Showing ${numberOfSignalsToBeClosed.toString()} signals` + ); cy.get(SIGNALS).should('have.length', numberOfSignalsToBeClosed); const numberOfSignalsToBeOpened = 1; selectNumberOfSignals(numberOfSignalsToBeOpened); - cy.get(SELECTED_SIGNALS) - .invoke('text') - .should('eql', `Selected ${numberOfSignalsToBeOpened} signal`); + cy.get(SELECTED_SIGNALS).should( + 'have.text', + `Selected ${numberOfSignalsToBeOpened} signal` + ); openSignals(); waitForSignals(); @@ -93,15 +95,14 @@ describe('Detections', () => { waitForSignals(); const expectedNumberOfClosedSignalsAfterOpened = 2; - cy.get(NUMBER_OF_SIGNALS) - .invoke('text') - .should('eql', expectedNumberOfClosedSignalsAfterOpened.toString()); - cy.get(SHOWING_SIGNALS) - .invoke('text') - .should( - 'eql', - `Showing ${expectedNumberOfClosedSignalsAfterOpened.toString()} signals` - ); + cy.get(NUMBER_OF_SIGNALS).should( + 'have.text', + expectedNumberOfClosedSignalsAfterOpened.toString() + ); + cy.get(SHOWING_SIGNALS).should( + 'have.text', + `Showing ${expectedNumberOfClosedSignalsAfterOpened.toString()} signals` + ); cy.get(SIGNALS).should('have.length', expectedNumberOfClosedSignalsAfterOpened); goToOpenedSignals(); @@ -109,13 +110,15 @@ describe('Detections', () => { const expectedNumberOfOpenedSignals = +numberOfSignals - expectedNumberOfClosedSignalsAfterOpened; - cy.get(SHOWING_SIGNALS) - .invoke('text') - .should('eql', `Showing ${expectedNumberOfOpenedSignals.toString()} signals`); - - cy.get('[data-test-subj="server-side-event-count"]') - .invoke('text') - .should('eql', expectedNumberOfOpenedSignals.toString()); + cy.get(SHOWING_SIGNALS).should( + 'have.text', + `Showing ${expectedNumberOfOpenedSignals.toString()} signals` + ); + + cy.get('[data-test-subj="server-side-event-count"]').should( + 'have.text', + expectedNumberOfOpenedSignals.toString() + ); }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts index f388ac1215d01..cb776be8d7b6b 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts @@ -10,7 +10,7 @@ export const LOADING_SIGNALS_PANEL = '[data-test-subj="loading-signals-panel"]'; export const MANAGE_SIGNAL_DETECTION_RULES_BTN = '[data-test-subj="manage-signal-detection-rules"]'; -export const NUMBER_OF_SIGNALS = '[data-test-subj="server-side-event-count"]'; +export const NUMBER_OF_SIGNALS = '[data-test-subj="server-side-event-count"] .euiBadge__text'; export const OPEN_CLOSE_SIGNAL_BTN = '[data-test-subj="update-signal-status-button"]'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx index c7e368da1338f..cbb4006bbf933 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx @@ -10,7 +10,10 @@ import { createPortalNode, InPortal } from 'react-reverse-portal'; import styled, { css } from 'styled-components'; import { npStart } from 'ui/new_platform'; -import { EmbeddablePanel } from '../../../../../../../src/plugins/embeddable/public'; +import { + EmbeddablePanel, + ErrorEmbeddable, +} from '../../../../../../../src/plugins/embeddable/public'; import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { getIndexPatternTitleIdMapping } from '../../hooks/api/helpers'; import { useIndexPatterns } from '../../hooks/use_index_patterns'; @@ -84,7 +87,9 @@ export const EmbeddedMapComponent = ({ setQuery, startDate, }: EmbeddedMapProps) => { - const [embeddable, setEmbeddable] = React.useState(null); + const [embeddable, setEmbeddable] = React.useState( + undefined + ); const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(false); const [isIndexError, setIsIndexError] = useState(false); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx index 0ffb13cd66028..f4e6ee5f878a6 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx @@ -20,8 +20,10 @@ jest.mock('ui/new_platform'); const { npStart } = createUiNewPlatformMock(); npStart.plugins.embeddable.getEmbeddableFactory = jest.fn().mockImplementation(() => ({ - createFromState: () => ({ + create: () => ({ reload: jest.fn(), + setRenderTooltipContent: jest.fn(), + setLayerList: jest.fn(), }), })); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx index 56211c9ff8935..0c7a1212ba280 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx @@ -11,10 +11,20 @@ import minimatch from 'minimatch'; import { IndexPatternMapping, SetQuery } from './types'; import { getLayerList } from './map_config'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../plugins/maps/public'; -import { MapEmbeddable, RenderTooltipContentParams } from '../../../../maps/public'; +import { + MapEmbeddable, + RenderTooltipContentParams, + MapEmbeddableInput, +} from '../../../../maps/public'; import * as i18n from './translations'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; -import { EmbeddableStart, ViewMode } from '../../../../../../../src/plugins/embeddable/public'; +import { + EmbeddableStart, + isErrorEmbeddable, + EmbeddableOutput, + ViewMode, + ErrorEmbeddable, +} from '../../../../../../../src/plugins/embeddable/public'; import { IndexPatternSavedObject } from '../../hooks/types'; /** @@ -40,14 +50,19 @@ export const createEmbeddable = async ( setQuery: SetQuery, portalNode: PortalNode, embeddableApi: EmbeddableStart -): Promise => { - const factory = embeddableApi.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); +): Promise => { + const factory = embeddableApi.getEmbeddableFactory< + MapEmbeddableInput, + EmbeddableOutput, + MapEmbeddable + >(MAP_SAVED_OBJECT_TYPE); - const state = { - layerList: getLayerList(indexPatterns), + if (!factory) { + throw new Error('Map embeddable factory undefined'); + } + + const input: MapEmbeddableInput = { title: i18n.MAP_TITLE, - }; - const input = { id: uuid.v4(), filters, hidePanelTitles: true, @@ -86,13 +101,16 @@ export const createEmbeddable = async ( return ; }; - // @ts-ignore method added in https://github.com/elastic/kibana/pull/43878 - const embeddableObject = await factory.createFromState( - state, - input, - undefined, - renderTooltipContent - ); + const embeddableObject = await factory.create(input); + + if (!embeddableObject) { + throw new Error('Map embeddable is undefined'); + } + + if (!isErrorEmbeddable(embeddableObject)) { + embeddableObject.setRenderTooltipContent(renderTooltipContent); + embeddableObject.setLayerList(getLayerList(indexPatterns)); + } // Wire up to app refresh action setQuery({ diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx index 05dfd561b1f5e..b00eef79ee480 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx @@ -7,13 +7,13 @@ import { EuiButtonEmpty, EuiCallOut, EuiPopover, EuiPopoverTitle, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; -import React, { useReducer, useState } from 'react'; +import React, { Dispatch, useCallback, useReducer, useState } from 'react'; import styled from 'styled-components'; import { useKibana } from '../../lib/kibana'; import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../lib/telemetry'; import { hasMlAdminPermissions } from '../ml/permissions/has_ml_admin_permissions'; -import { errorToToaster, useStateToaster } from '../toasters'; +import { errorToToaster, useStateToaster, ActionToaster } from '../toasters'; import { setupMlJob, startDatafeeds, stopDatafeeds } from './api'; import { filterJobs } from './helpers'; import { useSiemJobs } from './hooks/use_siem_jobs'; @@ -22,7 +22,7 @@ import { JobsTable } from './jobs_table/jobs_table'; import { ShowingCount } from './jobs_table/showing_count'; import { PopoverDescription } from './popover_description'; import * as i18n from './translations'; -import { JobsFilters, JobSummary, SiemJob } from './types'; +import { JobsFilters, SiemJob } from './types'; import { UpgradeContents } from './upgrade_contents'; import { useMlCapabilities } from './hooks/use_ml_capabilities'; @@ -34,15 +34,10 @@ PopoverContentsDiv.displayName = 'PopoverContentsDiv'; interface State { isLoading: boolean; - jobs: JobSummary[]; refreshToggle: boolean; } -type Action = - | { type: 'refresh' } - | { type: 'loading' } - | { type: 'success'; results: JobSummary[] } - | { type: 'failure' }; +type Action = { type: 'refresh' } | { type: 'loading' } | { type: 'success' } | { type: 'failure' }; function mlPopoverReducer(state: State, action: Action): State { switch (action.type) { @@ -62,14 +57,12 @@ function mlPopoverReducer(state: State, action: Action): State { return { ...state, isLoading: false, - jobs: action.results, }; } case 'failure': { return { ...state, isLoading: false, - jobs: [], }; } default: @@ -79,7 +72,6 @@ function mlPopoverReducer(state: State, action: Action): State { const initialState: State = { isLoading: false, - jobs: [], refreshToggle: true, }; @@ -91,7 +83,7 @@ const defaultFilterProps: JobsFilters = { }; export const MlPopover = React.memo(() => { - const [{ refreshToggle }, dispatch] = useReducer(mlPopoverReducer, initialState); + const [{ isLoading, refreshToggle }, dispatch] = useReducer(mlPopoverReducer, initialState); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [filterProperties, setFilterProperties] = useState(defaultFilterProps); @@ -99,50 +91,11 @@ export const MlPopover = React.memo(() => { const [, dispatchToaster] = useStateToaster(); const capabilities = useMlCapabilities(); const docLinks = useKibana().services.docLinks; - - // Enable/Disable Job & Datafeed -- passed to JobsTable for use as callback on JobSwitch - const enableDatafeed = async (job: SiemJob, latestTimestampMs: number, enable: boolean) => { - submitTelemetry(job, enable); - - if (!job.isInstalled) { - try { - await setupMlJob({ - configTemplate: job.moduleId, - indexPatternName: job.defaultIndexPattern, - jobIdErrorFilter: [job.id], - groups: job.groups, - }); - } catch (error) { - errorToToaster({ title: i18n.CREATE_JOB_FAILURE, error, dispatchToaster }); - dispatch({ type: 'refresh' }); - return; - } - } - - // Max start time for job is no more than two weeks ago to ensure job performance - const maxStartTime = moment - .utc() - .subtract(14, 'days') - .valueOf(); - - if (enable) { - const startTime = Math.max(latestTimestampMs, maxStartTime); - try { - await startDatafeeds({ datafeedIds: [`datafeed-${job.id}`], start: startTime }); - } catch (error) { - track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.JOB_ENABLE_FAILURE); - errorToToaster({ title: i18n.START_JOB_FAILURE, error, dispatchToaster }); - } - } else { - try { - await stopDatafeeds({ datafeedIds: [`datafeed-${job.id}`] }); - } catch (error) { - track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.JOB_DISABLE_FAILURE); - errorToToaster({ title: i18n.STOP_JOB_FAILURE, error, dispatchToaster }); - } - } - dispatch({ type: 'refresh' }); - }; + const handleJobStateChange = useCallback( + (job: SiemJob, latestTimestampMs: number, enable: boolean) => + enableDatafeed(job, latestTimestampMs, enable, dispatch, dispatchToaster), + [dispatch, dispatchToaster] + ); const filteredJobs = filterJobs({ jobs: siemJobs, @@ -239,9 +192,9 @@ export const MlPopover = React.memo(() => { )} @@ -252,6 +205,59 @@ export const MlPopover = React.memo(() => { } }); +// Enable/Disable Job & Datafeed -- passed to JobsTable for use as callback on JobSwitch +const enableDatafeed = async ( + job: SiemJob, + latestTimestampMs: number, + enable: boolean, + dispatch: Dispatch, + dispatchToaster: Dispatch +) => { + submitTelemetry(job, enable); + + if (!job.isInstalled) { + dispatch({ type: 'loading' }); + try { + await setupMlJob({ + configTemplate: job.moduleId, + indexPatternName: job.defaultIndexPattern, + jobIdErrorFilter: [job.id], + groups: job.groups, + }); + dispatch({ type: 'success' }); + } catch (error) { + errorToToaster({ title: i18n.CREATE_JOB_FAILURE, error, dispatchToaster }); + dispatch({ type: 'failure' }); + dispatch({ type: 'refresh' }); + return; + } + } + + // Max start time for job is no more than two weeks ago to ensure job performance + const maxStartTime = moment + .utc() + .subtract(14, 'days') + .valueOf(); + + if (enable) { + const startTime = Math.max(latestTimestampMs, maxStartTime); + try { + await startDatafeeds({ datafeedIds: [`datafeed-${job.id}`], start: startTime }); + } catch (error) { + track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.JOB_ENABLE_FAILURE); + errorToToaster({ title: i18n.START_JOB_FAILURE, error, dispatchToaster }); + } + } else { + try { + await stopDatafeeds({ datafeedIds: [`datafeed-${job.id}`] }); + } catch (error) { + track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.JOB_DISABLE_FAILURE); + errorToToaster({ title: i18n.STOP_JOB_FAILURE, error, dispatchToaster }); + } + } + dispatch({ type: 'refresh' }); +}; + const submitTelemetry = (job: SiemJob, enabled: boolean) => { // Report type of job enabled/disabled track( diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx index 426a1ab9238dc..4d9e283bfb9cc 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx @@ -22,7 +22,6 @@ describe('usePersistRule', () => { usePrePackagedRules({ canUserCRUD: null, hasIndexWrite: null, - hasManageApiKey: null, isAuthenticated: null, hasEncryptionKey: null, isSignalIndexExists: null, @@ -50,7 +49,6 @@ describe('usePersistRule', () => { usePrePackagedRules({ canUserCRUD: null, hasIndexWrite: null, - hasManageApiKey: null, isAuthenticated: null, hasEncryptionKey: null, isSignalIndexExists: null, @@ -79,7 +77,6 @@ describe('usePersistRule', () => { usePrePackagedRules({ canUserCRUD: true, hasIndexWrite: true, - hasManageApiKey: true, isAuthenticated: true, hasEncryptionKey: true, isSignalIndexExists: true, @@ -116,7 +113,6 @@ describe('usePersistRule', () => { usePrePackagedRules({ canUserCRUD: true, hasIndexWrite: true, - hasManageApiKey: true, isAuthenticated: true, hasEncryptionKey: true, isSignalIndexExists: true, @@ -139,7 +135,6 @@ describe('usePersistRule', () => { usePrePackagedRules({ canUserCRUD: false, hasIndexWrite: true, - hasManageApiKey: true, isAuthenticated: true, hasEncryptionKey: true, isSignalIndexExists: true, @@ -161,29 +156,6 @@ describe('usePersistRule', () => { usePrePackagedRules({ canUserCRUD: true, hasIndexWrite: false, - hasManageApiKey: true, - isAuthenticated: true, - hasEncryptionKey: true, - isSignalIndexExists: true, - }) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - let resp = null; - if (result.current.createPrePackagedRules) { - resp = await result.current.createPrePackagedRules(); - } - expect(resp).toEqual(false); - }); - }); - - test('can NOT createPrePackagedRules because hasManageApiKey === false', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePrePackagedRules({ - canUserCRUD: true, - hasIndexWrite: true, - hasManageApiKey: false, isAuthenticated: true, hasEncryptionKey: true, isSignalIndexExists: true, @@ -205,7 +177,6 @@ describe('usePersistRule', () => { usePrePackagedRules({ canUserCRUD: true, hasIndexWrite: true, - hasManageApiKey: true, isAuthenticated: false, hasEncryptionKey: true, isSignalIndexExists: true, @@ -227,7 +198,6 @@ describe('usePersistRule', () => { usePrePackagedRules({ canUserCRUD: true, hasIndexWrite: true, - hasManageApiKey: true, isAuthenticated: true, hasEncryptionKey: false, isSignalIndexExists: true, @@ -249,7 +219,6 @@ describe('usePersistRule', () => { usePrePackagedRules({ canUserCRUD: true, hasIndexWrite: true, - hasManageApiKey: true, isAuthenticated: true, hasEncryptionKey: true, isSignalIndexExists: false, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx index 0dd95bea8a0b2..44d5de10e361a 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -26,7 +26,6 @@ export interface ReturnPrePackagedRules { interface UsePrePackagedRuleProps { canUserCRUD: boolean | null; hasIndexWrite: boolean | null; - hasManageApiKey: boolean | null; isAuthenticated: boolean | null; hasEncryptionKey: boolean | null; isSignalIndexExists: boolean | null; @@ -36,7 +35,6 @@ interface UsePrePackagedRuleProps { * Hook for using to get status about pre-packaged Rules from the Detection Engine API * * @param hasIndexWrite boolean - * @param hasManageApiKey boolean * @param isAuthenticated boolean * @param hasEncryptionKey boolean * @param isSignalIndexExists boolean @@ -45,7 +43,6 @@ interface UsePrePackagedRuleProps { export const usePrePackagedRules = ({ canUserCRUD, hasIndexWrite, - hasManageApiKey, isAuthenticated, hasEncryptionKey, isSignalIndexExists, @@ -117,7 +114,6 @@ export const usePrePackagedRules = ({ if ( canUserCRUD && hasIndexWrite && - hasManageApiKey && isAuthenticated && hasEncryptionKey && isSignalIndexExists @@ -185,14 +181,7 @@ export const usePrePackagedRules = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [ - canUserCRUD, - hasIndexWrite, - hasManageApiKey, - isAuthenticated, - hasEncryptionKey, - isSignalIndexExists, - ]); + }, [canUserCRUD, hasIndexWrite, isAuthenticated, hasEncryptionKey, isSignalIndexExists]); return { loading, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx index 0d37cce1fd85c..412fc0706b151 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx @@ -16,7 +16,7 @@ type Func = (ruleId: string) => void; export type ReturnRuleStatus = [boolean, RuleStatus | null, Func | null]; export interface ReturnRulesStatuses { loading: boolean; - rulesStatuses: RuleStatusRowItemType[] | null; + rulesStatuses: RuleStatusRowItemType[]; } /** diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/mock.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/mock.ts index 37e93b1481e15..6b0c7e0078268 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/mock.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/mock.ts @@ -992,7 +992,6 @@ export const mockUserPrivilege: Privilege = { monitor_watcher: true, monitor_transform: true, read_ilm: true, - manage_api_key: true, manage_security: true, manage_own_api_key: false, manage_saml: true, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts index d90f94d32001d..4e97c597546a7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts @@ -54,7 +54,6 @@ export interface Privilege { monitor_watcher: boolean; monitor_transform: boolean; read_ilm: boolean; - manage_api_key: boolean; manage_security: boolean; manage_own_api_key: boolean; manage_saml: boolean; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx index 2682742960442..c248223c6b81b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx @@ -21,7 +21,6 @@ describe('usePrivilegeUser', () => { hasEncryptionKey: null, hasIndexManage: null, hasIndexWrite: null, - hasManageApiKey: null, isAuthenticated: null, loading: true, }); @@ -39,7 +38,6 @@ describe('usePrivilegeUser', () => { hasEncryptionKey: true, hasIndexManage: true, hasIndexWrite: true, - hasManageApiKey: true, isAuthenticated: true, loading: false, }); @@ -61,7 +59,6 @@ describe('usePrivilegeUser', () => { hasEncryptionKey: false, hasIndexManage: false, hasIndexWrite: false, - hasManageApiKey: false, isAuthenticated: false, loading: false, }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx index c58e62c062fae..140dd1544b12b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx @@ -15,7 +15,6 @@ export interface ReturnPrivilegeUser { isAuthenticated: boolean | null; hasEncryptionKey: boolean | null; hasIndexManage: boolean | null; - hasManageApiKey: boolean | null; hasIndexWrite: boolean | null; } /** @@ -27,17 +26,12 @@ export const usePrivilegeUser = (): ReturnPrivilegeUser => { const [privilegeUser, setPrivilegeUser] = useState< Pick< ReturnPrivilegeUser, - | 'isAuthenticated' - | 'hasEncryptionKey' - | 'hasIndexManage' - | 'hasManageApiKey' - | 'hasIndexWrite' + 'isAuthenticated' | 'hasEncryptionKey' | 'hasIndexManage' | 'hasIndexWrite' > >({ isAuthenticated: null, hasEncryptionKey: null, hasIndexManage: null, - hasManageApiKey: null, hasIndexWrite: null, }); const [, dispatchToaster] = useStateToaster(); @@ -65,10 +59,6 @@ export const usePrivilegeUser = (): ReturnPrivilegeUser => { privilege.index[indexName].create_doc || privilege.index[indexName].index || privilege.index[indexName].write, - hasManageApiKey: - privilege.cluster.manage_security || - privilege.cluster.manage_api_key || - privilege.cluster.manage_own_api_key, }); } } @@ -78,7 +68,6 @@ export const usePrivilegeUser = (): ReturnPrivilegeUser => { isAuthenticated: false, hasEncryptionKey: false, hasIndexManage: false, - hasManageApiKey: false, hasIndexWrite: false, }); errorToToaster({ title: i18n.PRIVILEGE_FETCH_FAILURE, error, dispatchToaster }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx index a96913f2ad541..9e45371fb6058 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx @@ -15,7 +15,6 @@ export interface State { canUserCRUD: boolean | null; hasIndexManage: boolean | null; hasIndexWrite: boolean | null; - hasManageApiKey: boolean | null; isSignalIndexExists: boolean | null; isAuthenticated: boolean | null; hasEncryptionKey: boolean | null; @@ -27,7 +26,6 @@ const initialState: State = { canUserCRUD: null, hasIndexManage: null, hasIndexWrite: null, - hasManageApiKey: null, isSignalIndexExists: null, isAuthenticated: null, hasEncryptionKey: null, @@ -37,10 +35,6 @@ const initialState: State = { export type Action = | { type: 'updateLoading'; loading: boolean } - | { - type: 'updateHasManageApiKey'; - hasManageApiKey: boolean | null; - } | { type: 'updateHasIndexManage'; hasIndexManage: boolean | null; @@ -90,12 +84,6 @@ export const userInfoReducer = (state: State, action: Action): State => { hasIndexWrite: action.hasIndexWrite, }; } - case 'updateHasManageApiKey': { - return { - ...state, - hasManageApiKey: action.hasManageApiKey, - }; - } case 'updateIsSignalIndexExists': { return { ...state, @@ -151,7 +139,6 @@ export const useUserInfo = (): State => { canUserCRUD, hasIndexManage, hasIndexWrite, - hasManageApiKey, isSignalIndexExists, isAuthenticated, hasEncryptionKey, @@ -166,7 +153,6 @@ export const useUserInfo = (): State => { hasEncryptionKey: isApiEncryptionKey, hasIndexManage: hasApiIndexManage, hasIndexWrite: hasApiIndexWrite, - hasManageApiKey: hasApiManageApiKey, } = usePrivilegeUser(); const { loading: indexNameLoading, @@ -197,12 +183,6 @@ export const useUserInfo = (): State => { } }, [loading, hasIndexWrite, hasApiIndexWrite]); - useEffect(() => { - if (!loading && hasManageApiKey !== hasApiManageApiKey && hasApiManageApiKey != null) { - dispatch({ type: 'updateHasManageApiKey', hasManageApiKey: hasApiManageApiKey }); - } - }, [loading, hasManageApiKey, hasApiManageApiKey]); - useEffect(() => { if ( !loading && @@ -258,7 +238,6 @@ export const useUserInfo = (): State => { canUserCRUD, hasIndexManage, hasIndexWrite, - hasManageApiKey, signalIndexName, }; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index 5157bd81403e2..9a84d33ab5fdf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -81,8 +81,8 @@ export type RuleStatusRowItemType = RuleStatus & { name: string; id: string; }; -type RulesColumns = EuiBasicTableColumn | EuiTableActionsColumnType; -type RulesStatusesColumns = EuiBasicTableColumn; +export type RulesColumns = EuiBasicTableColumn | EuiTableActionsColumnType; +export type RulesStatusesColumns = EuiBasicTableColumn; interface GetColumns { dispatch: React.Dispatch; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx index 1a98272546440..ccdfd1ed1be38 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx @@ -31,7 +31,7 @@ import { Loader } from '../../../../components/loader'; import { Panel } from '../../../../components/panel'; import { PrePackagedRulesPrompt } from '../components/pre_packaged_rules/load_empty_prompt'; import { GenericDownloader } from '../../../../components/generic_downloader'; -import { AllRulesTables } from '../components/all_rules_tables'; +import { AllRulesTables, SortingType } from '../components/all_rules_tables'; import { getPrePackagedRuleStatus } from '../helpers'; import * as i18n from '../translations'; import { EuiBasicTableOnChange } from '../types'; @@ -128,7 +128,7 @@ export const AllRules = React.memo( }); const sorting = useMemo( - () => ({ sort: { field: 'enabled', direction: filterOptions.sortOrder } }), + (): SortingType => ({ sort: { field: 'enabled', direction: filterOptions.sortOrder } }), [filterOptions.sortOrder] ); @@ -330,7 +330,7 @@ export const AllRules = React.memo( euiBasicTableSelectionProps={euiBasicTableSelectionProps} hasNoPermissions={hasNoPermissions} monitoringColumns={monitoringColumns} - paginationMemo={paginationMemo} + pagination={paginationMemo} rules={rules} rulesColumns={rulesColumns} rulesStatuses={rulesStatuses} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx index 0fd07f30a00b6..31aaa426e4f3b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx @@ -4,30 +4,59 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBasicTable, EuiTab, EuiTabs, EuiEmptyPrompt } from '@elastic/eui'; +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiTab, + EuiTabs, + EuiEmptyPrompt, + Direction, + EuiTableSelectionType, +} from '@elastic/eui'; import React, { useMemo, memo, useState } from 'react'; import styled from 'styled-components'; +import { EuiBasicTableOnChange } from '../../types'; import * as i18n from '../../translations'; -import { RuleStatusRowItemType } from '../../../../../pages/detection_engine/rules/all/columns'; -import { Rules } from '../../../../../containers/detection_engine/rules'; +import { + RulesColumns, + RuleStatusRowItemType, +} from '../../../../../pages/detection_engine/rules/all/columns'; +import { Rule, Rules } from '../../../../../containers/detection_engine/rules'; // EuiBasicTable give me a hardtime with adding the ref attributes so I went the easy way // after few hours of fight with typescript !!!! I lost :( // eslint-disable-next-line @typescript-eslint/no-explicit-any const MyEuiBasicTable = styled(EuiBasicTable as any)`` as any; +export interface SortingType { + sort: { + field: 'enabled'; + direction: Direction; + }; +} + interface AllRulesTablesProps { - euiBasicTableSelectionProps: unknown; + euiBasicTableSelectionProps: EuiTableSelectionType; hasNoPermissions: boolean; - monitoringColumns: unknown; - paginationMemo: unknown; + monitoringColumns: Array>; + pagination: { + pageIndex: number; + pageSize: number; + totalItemCount: number; + pageSizeOptions: number[]; + }; rules: Rules; - rulesColumns: unknown; - rulesStatuses: RuleStatusRowItemType[] | null; - sorting: unknown; - tableOnChangeCallback: unknown; - tableRef?: unknown; + rulesColumns: RulesColumns[]; + rulesStatuses: RuleStatusRowItemType[]; + sorting: { + sort: { + field: 'enabled'; + direction: Direction; + }; + }; + tableOnChangeCallback: ({ page, sort }: EuiBasicTableOnChange) => void; + tableRef?: React.MutableRefObject; } enum AllRulesTabs { @@ -52,7 +81,7 @@ const AllRulesTablesComponent: React.FC = ({ euiBasicTableSelectionProps, hasNoPermissions, monitoringColumns, - paginationMemo, + pagination, rules, rulesColumns, rulesStatuses, @@ -95,7 +124,7 @@ const AllRulesTablesComponent: React.FC = ({ items={rules ?? []} noItemsMessage={emptyPrompt} onChange={tableOnChangeCallback} - pagination={paginationMemo} + pagination={pagination} ref={tableRef} sorting={sorting} selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps} @@ -110,7 +139,7 @@ const AllRulesTablesComponent: React.FC = ({ items={rulesStatuses} noItemsMessage={emptyPrompt} onChange={tableOnChangeCallback} - pagination={paginationMemo} + pagination={pagination} sorting={sorting} /> )} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap index 65a606604d4a7..1bee36ed9e185 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RuleActionsOverflow renders correctly against snapshot 1`] = ` +exports[`RuleActionsOverflow snapshots renders correctly against snapshot 1`] = ` } closePopover={[Function]} + data-test-subj="rules-details-popover" display="inlineBlock" hasArrow={true} id="ruleActionsOverflow" @@ -27,24 +29,28 @@ exports[`RuleActionsOverflow renders correctly against snapshot 1`] = ` panelPaddingSize="none" > Duplicate rule… , Export rule , ({ }), })); +jest.mock('../../all/actions', () => ({ + deleteRulesAction: jest.fn(), + duplicateRulesAction: jest.fn(), +})); + describe('RuleActionsOverflow', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); + describe('snapshots', () => { + test('renders correctly against snapshot', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + }); + + describe('rules details menu panel', () => { + test('there is at least one item when there is a rule within the rules-details-menu-panel', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + const items: unknown[] = wrapper + .find('[data-test-subj="rules-details-menu-panel"]') + .first() + .prop('items'); + + expect(items.length).toBeGreaterThan(0); + }); + + test('items are empty when there is a null rule within the rules-details-menu-panel', () => { + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-menu-panel"]') + .first() + .prop('items') + ).toEqual([]); + }); + + test('items are empty when there is an undefined rule within the rules-details-menu-panel', () => { + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-menu-panel"]') + .first() + .prop('items') + ).toEqual([]); + }); + + test('it opens the popover when rules-details-popover-button-icon is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(true); + }); + }); + + describe('rules details pop over button icon', () => { + test('it does not open the popover when rules-details-popover-button-icon is clicked when the user does not have permission', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(false); + }); + }); + + describe('rules details duplicate rule', () => { + test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect(wrapper.find('[data-test-subj="rules-details-delete-rule"] button').exists()).toEqual( + false + ); + }); + + test('it opens the popover when rules-details-popover-button-icon is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(true); + }); + + test('it closes the popover when rules-details-duplicate-rule is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(false); + }); + + test('it calls duplicateRulesAction when rules-details-duplicate-rule is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click'); + wrapper.update(); + expect(duplicateRulesAction).toHaveBeenCalled(); + }); + + test('it calls duplicateRulesAction with the rule and rule.id when rules-details-duplicate-rule is clicked', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click'); + wrapper.update(); + expect(duplicateRulesAction).toHaveBeenCalledWith( + [rule], + [rule.id], + expect.anything(), + expect.anything() + ); + }); + }); + + describe('rules details export rule', () => { + test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect(wrapper.find('[data-test-subj="rules-details-export-rule"] button').exists()).toEqual( + false + ); + }); + + test('it closes the popover when rules-details-export-rule is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(false); + }); + + test('it sets the rule.rule_id on the generic downloader when rules-details-export-rule is clicked', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper.find('[data-test-subj="rules-details-generic-downloader"]').prop('ids') + ).toEqual([rule.rule_id]); + }); + + test('it does not close the pop over on rules-details-export-rule when the rule is an immutable rule and the user does a click', () => { + const rule = mockRule('id'); + rule.immutable = true; + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(true); + }); + + test('it does not set the rule.rule_id on rules-details-export-rule when the rule is an immutable rule', () => { + const rule = mockRule('id'); + rule.immutable = true; + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper.find('[data-test-subj="rules-details-generic-downloader"]').prop('ids') + ).toEqual([]); + }); + }); + + describe('rules details delete rule', () => { + test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect(wrapper.find('[data-test-subj="rules-details-delete-rule"] button').exists()).toEqual( + false + ); + }); + + test('it closes the popover when rules-details-delete-rule is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(false); + }); + + test('it calls deleteRulesAction when rules-details-delete-rule is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click'); + wrapper.update(); + expect(deleteRulesAction).toHaveBeenCalled(); + }); + + test('it calls deleteRulesAction with the rule.id when rules-details-delete-rule is clicked', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click'); + wrapper.update(); + expect(deleteRulesAction).toHaveBeenCalledWith( + [rule.id], + expect.anything(), + expect.anything(), + expect.anything() + ); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx index e1ca84ed8cc64..a7ce0c85ffdcf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx @@ -62,8 +62,9 @@ const RuleActionsOverflowComponent = ({ ? [ { setIsPopoverOpen(false); await duplicateRulesAction([rule], [rule.id], noop, dispatchToaster); @@ -73,11 +74,12 @@ const RuleActionsOverflowComponent = ({ , { setIsPopoverOpen(false); - setRulesToExport([rule.id]); + setRulesToExport([rule.rule_id]); }} > {i18nActions.EXPORT_RULE} @@ -86,6 +88,7 @@ const RuleActionsOverflowComponent = ({ key={i18nActions.DELETE_RULE} icon="trash" disabled={userHasNoPermissions} + data-test-subj="rules-details-delete-rule" onClick={async () => { setIsPopoverOpen(false); await deleteRulesAction([rule.id], noop, dispatchToaster, onRuleDeletedCallback); @@ -109,6 +112,7 @@ const RuleActionsOverflowComponent = ({ iconType="boxesHorizontal" aria-label={i18n.ALL_ACTIONS} isDisabled={userHasNoPermissions} + data-test-subj="rules-details-popover-button-icon" onClick={handlePopoverOpen} /> @@ -124,15 +128,17 @@ const RuleActionsOverflowComponent = ({ closePopover={() => setIsPopoverOpen(false)} id="ruleActionsOverflow" isOpen={isPopoverOpen} + data-test-subj="rules-details-popover" ownFocus={true} panelPaddingSize="none" > - + { displaySuccessToast( i18nActions.SUCCESSFULLY_EXPORTED_RULES(exportCount), diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index 0335216672915..2686bb47925b6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -24,7 +24,7 @@ import { StepScheduleRule } from '../components/step_schedule_rule'; import { StepRuleActions } from '../components/step_rule_actions'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; import * as RuleI18n from '../translations'; -import { redirectToDetections, getActionMessageParams } from '../helpers'; +import { redirectToDetections, getActionMessageParams, userHasNoPermissions } from '../helpers'; import { AboutStepRule, DefineStepRule, @@ -85,7 +85,6 @@ const CreateRulePageComponent: React.FC = () => { isAuthenticated, hasEncryptionKey, canUserCRUD, - hasManageApiKey, } = useUserInfo(); const [, dispatchToaster] = useStateToaster(); const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); @@ -117,8 +116,6 @@ const CreateRulePageComponent: React.FC = () => { getActionMessageParams((stepsData.current['define-rule'].data as DefineStepRule).ruleType), [stepsData.current['define-rule'].data] ); - const userHasNoPermissions = - canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; const setStepData = useCallback( (step: RuleStep, data: unknown, isValid: boolean) => { @@ -274,7 +271,7 @@ const CreateRulePageComponent: React.FC = () => { if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { return ; - } else if (userHasNoPermissions) { + } else if (userHasNoPermissions(canUserCRUD)) { return ; } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index b8e2310ef0614..cb4d88a8bb539 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -53,7 +53,7 @@ import * as detectionI18n from '../../translations'; import { ReadOnlyCallOut } from '../components/read_only_callout'; import { RuleSwitch } from '../components/rule_switch'; import { StepPanel } from '../components/step_panel'; -import { getStepsData, redirectToDetections } from '../helpers'; +import { getStepsData, redirectToDetections, userHasNoPermissions } from '../helpers'; import * as ruleI18n from '../translations'; import * as i18n from './translations'; import { GlobalTime } from '../../../../containers/global_time'; @@ -96,7 +96,6 @@ const RuleDetailsPageComponent: FC = ({ isAuthenticated, hasEncryptionKey, canUserCRUD, - hasManageApiKey, hasIndexWrite, signalIndexName, } = useUserInfo(); @@ -115,8 +114,6 @@ const RuleDetailsPageComponent: FC = ({ scheduleRuleData: null, }; const [lastSignals] = useSignalInfo({ ruleId }); - const userHasNoPermissions = - canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; const title = isLoading === true || rule === null ? : rule.name; const subTitle = useMemo( @@ -227,7 +224,7 @@ const RuleDetailsPageComponent: FC = ({ return ( <> {hasIndexWrite != null && !hasIndexWrite && } - {userHasNoPermissions && } + {userHasNoPermissions(canUserCRUD) && } {({ indicesExist, indexPattern }) => { return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( @@ -264,7 +261,7 @@ const RuleDetailsPageComponent: FC = ({ = ({ {ruleI18n.EDIT_RULE_SETTINGS} @@ -285,7 +282,7 @@ const RuleDetailsPageComponent: FC = ({ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 60d6158987a1d..c42e7b902cd5c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -33,7 +33,12 @@ import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; import { StepRuleActions } from '../components/step_rule_actions'; import { formatRule } from '../create/helpers'; -import { getStepsData, redirectToDetections, getActionMessageParams } from '../helpers'; +import { + getStepsData, + redirectToDetections, + getActionMessageParams, + userHasNoPermissions, +} from '../helpers'; import * as ruleI18n from '../translations'; import { RuleStep, @@ -69,14 +74,10 @@ const EditRulePageComponent: FC = () => { isAuthenticated, hasEncryptionKey, canUserCRUD, - hasManageApiKey, } = useUserInfo(); const { detailName: ruleId } = useParams(); const [loading, rule] = useRule(ruleId); - const userHasNoPermissions = - canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; - const [initForm, setInitForm] = useState(false); const [myAboutRuleForm, setMyAboutRuleForm] = useState({ data: null, @@ -346,7 +347,7 @@ const EditRulePageComponent: FC = () => { if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { return ; - } else if (userHasNoPermissions) { + } else if (userHasNoPermissions(canUserCRUD)) { return ; } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx index 522464d585cca..443dbd2c93a35 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx @@ -14,6 +14,7 @@ import { getHumanizedDuration, getModifiedAboutDetailsData, determineDetailsValue, + userHasNoPermissions, } from './helpers'; import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; @@ -337,4 +338,27 @@ describe('rule helpers', () => { expect(result).toEqual(aboutRuleDetailsData); }); }); + + describe('userHasNoPermissions', () => { + test("returns false when user's CRUD operations are null", () => { + const result: boolean = userHasNoPermissions(null); + const userHasNoPermissionsExpectedResult = false; + + expect(result).toEqual(userHasNoPermissionsExpectedResult); + }); + + test('returns true when user cannot CRUD', () => { + const result: boolean = userHasNoPermissions(false); + const userHasNoPermissionsExpectedResult = true; + + expect(result).toEqual(userHasNoPermissionsExpectedResult); + }); + + test('returns false when user can CRUD', () => { + const result: boolean = userHasNoPermissions(true); + const userHasNoPermissionsExpectedResult = false; + + expect(result).toEqual(userHasNoPermissionsExpectedResult); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index b6afba527ccdc..db1f2298b5ea7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -267,3 +267,7 @@ export const getActionMessageParams = memoizeOne((ruleType: RuleType | undefined ...actionMessageRuleParams.map(param => `context.rule.${param}`), ]; }); + +// typed as null not undefined as the initial state for this value is null. +export const userHasNoPermissions = (canUserCRUD: boolean | null): boolean => + canUserCRUD != null ? !canUserCRUD : false; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx index 2b93ec8b10112..8831bc77691fa 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx @@ -23,7 +23,7 @@ import { AllRules } from './all'; import { ImportDataModal } from '../../../components/import_data_modal'; import { ReadOnlyCallOut } from './components/read_only_callout'; import { UpdatePrePackagedRulesCallOut } from './components/pre_packaged_rules/update_callout'; -import { getPrePackagedRuleStatus, redirectToDetections } from './helpers'; +import { getPrePackagedRuleStatus, redirectToDetections, userHasNoPermissions } from './helpers'; import * as i18n from './translations'; type Func = (refreshPrePackagedRule?: boolean) => void; @@ -38,7 +38,6 @@ const RulesPageComponent: React.FC = () => { hasEncryptionKey, canUserCRUD, hasIndexWrite, - hasManageApiKey, } = useUserInfo(); const { createPrePackagedRules, @@ -52,7 +51,6 @@ const RulesPageComponent: React.FC = () => { } = usePrePackagedRules({ canUserCRUD, hasIndexWrite, - hasManageApiKey, isSignalIndexExists, isAuthenticated, hasEncryptionKey, @@ -63,9 +61,6 @@ const RulesPageComponent: React.FC = () => { rulesNotUpdated ); - const userHasNoPermissions = - canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; - const handleRefreshRules = useCallback(async () => { if (refreshRulesData.current != null) { refreshRulesData.current(true); @@ -95,7 +90,7 @@ const RulesPageComponent: React.FC = () => { return ( <> - {userHasNoPermissions && } + {userHasNoPermissions(canUserCRUD) && } setShowImportModal(false)} @@ -125,7 +120,7 @@ const RulesPageComponent: React.FC = () => { {i18n.LOAD_PREPACKAGED_RULES} @@ -138,7 +133,7 @@ const RulesPageComponent: React.FC = () => { data-test-subj="reloadPrebuiltRulesBtn" iconType="plusInCircle" isLoading={loadingCreatePrePackagedRules} - isDisabled={userHasNoPermissions || loading} + isDisabled={userHasNoPermissions(canUserCRUD) || loading} onClick={handleCreatePrePackagedRules} > {i18n.RELOAD_MISSING_PREPACKAGED_RULES(rulesNotInstalled ?? 0)} @@ -148,7 +143,7 @@ const RulesPageComponent: React.FC = () => { { setShowImportModal(true); }} @@ -162,7 +157,7 @@ const RulesPageComponent: React.FC = () => { fill href={getCreateRuleUrl()} iconType="plusInCircle" - isDisabled={userHasNoPermissions || loading} + isDisabled={userHasNoPermissions(canUserCRUD) || loading} > {i18n.ADD_NEW_RULE} @@ -180,7 +175,7 @@ const RulesPageComponent: React.FC = () => { createPrePackagedRules={createPrePackagedRules} loading={loading || prePackagedRuleLoading} loadingCreatePrePackagedRules={loadingCreatePrePackagedRules} - hasNoPermissions={userHasNoPermissions} + hasNoPermissions={userHasNoPermissions(canUserCRUD)} refetchPrePackagedRulesStatus={handleRefetchPrePackagedRulesStatus} rulesCustomInstalled={rulesCustomInstalled} rulesInstalled={rulesInstalled} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts index 8e4b5ce3c9924..bdbb6ff7d1052 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts @@ -28,25 +28,23 @@ describe('buildRuleMessageFactory', () => { expect(message).toEqual(expect.stringContaining('signals index: "index"')); }); - it('joins message parts with newlines', () => { + it('joins message parts with spaces', () => { const buildMessage = buildRuleMessageFactory(factoryParams); const message = buildMessage('my message'); - const messageParts = message.split('\n'); - expect(messageParts).toContain('my message'); - expect(messageParts).toContain('name: "name"'); - expect(messageParts).toContain('id: "id"'); - expect(messageParts).toContain('rule id: "ruleId"'); - expect(messageParts).toContain('signals index: "index"'); + expect(message).toEqual(expect.stringContaining('my message ')); + expect(message).toEqual(expect.stringContaining(' name: "name" ')); + expect(message).toEqual(expect.stringContaining(' id: "id" ')); + expect(message).toEqual(expect.stringContaining(' rule id: "ruleId" ')); + expect(message).toEqual(expect.stringContaining(' signals index: "index"')); }); - it('joins multiple arguments with newlines', () => { + it('joins multiple arguments with spaces', () => { const buildMessage = buildRuleMessageFactory(factoryParams); const message = buildMessage('my message', 'here is more'); - const messageParts = message.split('\n'); - expect(messageParts).toContain('my message'); - expect(messageParts).toContain('here is more'); + expect(message).toEqual(expect.stringContaining('my message ')); + expect(message).toEqual(expect.stringContaining(' here is more')); }); it('defaults the rule ID if not provided ', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts index d5f9d332bbcdd..cc97a1f8a9f0b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts @@ -24,4 +24,4 @@ export const buildRuleMessageFactory = ({ `id: "${id}"`, `rule id: "${ruleId ?? '(unknown rule id)'}"`, `signals index: "${index}"`, - ].join('\n'); + ].join(' '); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 91905722fbca3..246701e94c99a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -55,6 +55,7 @@ export const signalRulesAlertType = ({ index, filters, language, + maxSignals, meta, machineLearningJobId, outputIndex, @@ -63,6 +64,14 @@ export const signalRulesAlertType = ({ to, type, } = params; + const searchAfterSize = Math.min(maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE); + let hasError: boolean = false; + let result: SearchAfterAndBulkCreateReturnType = { + success: false, + bulkCreateTimes: [], + searchAfterTimes: [], + lastLookBackDate: null, + }; const ruleStatusClient = ruleStatusSavedObjectsClientFactory(services.savedObjectsClient); const ruleStatusService = await ruleStatusServiceFactory({ alertId, @@ -104,17 +113,10 @@ export const signalRulesAlertType = ({ ); logger.warn(gapMessage); + hasError = true; await ruleStatusService.error(gapMessage, { gap: gapString }); } - const searchAfterSize = Math.min(params.maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE); - let result: SearchAfterAndBulkCreateReturnType = { - success: false, - bulkCreateTimes: [], - searchAfterTimes: [], - lastLookBackDate: null, - }; - try { if (isMlRule(type)) { if (ml == null) { @@ -126,7 +128,7 @@ export const signalRulesAlertType = ({ 'Machine learning rule is missing job id and/or anomaly threshold:', `job id: "${machineLearningJobId}"`, `anomaly threshold: "${anomalyThreshold}"`, - ].join('\n') + ].join(' ') ); } @@ -143,6 +145,7 @@ export const signalRulesAlertType = ({ `datafeed status: "${jobSummary?.datafeedState}"` ); logger.warn(errorMessage); + hasError = true; await ruleStatusService.error(errorMessage); } @@ -270,11 +273,13 @@ export const signalRulesAlertType = ({ } logger.debug(buildRuleMessage('[+] Signal Rule execution completed.')); - await ruleStatusService.success('succeeded', { - bulkCreateTimeDurations: result.bulkCreateTimes, - searchAfterTimeDurations: result.searchAfterTimes, - lastLookBackDate: result.lastLookBackDate?.toISOString(), - }); + if (!hasError) { + await ruleStatusService.success('succeeded', { + bulkCreateTimeDurations: result.bulkCreateTimes, + searchAfterTimeDurations: result.searchAfterTimes, + lastLookBackDate: result.lastLookBackDate?.toISOString(), + }); + } } else { const errorMessage = buildRuleMessage( 'Bulk Indexing of signals failed. Check logs for further details.' diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts index 6552f973a66fa..fc87a775a9e68 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts @@ -139,9 +139,9 @@ export const kqlQuery = Joi.object({ kuery: Joi.object({ kind: allowEmptyString, expression: allowEmptyString, - }), + }).allow(null), serializedQuery: allowEmptyString, - }), + }).allow(null), }); export const pinnedEventIds = Joi.array() .items(allowEmptyString) diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts index 5596d0c70f5ea..f69a715f9b2c9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts @@ -127,7 +127,7 @@ export const saveNotes = ( existingNoteIds?: string[], newNotes?: NoteResult[] ) => { - return ( + return Promise.all( newNotes?.map(note => { const newNote: SavedNote = { eventId: note.eventId, diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 757c1eb557c54..8d44c17018255 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -12,9 +12,7 @@ import { SpacesServiceSetup } from '../../../plugins/spaces/server'; import { SpacesPluginSetup } from '../../../plugins/spaces/server'; // @ts-ignore import { AuditLogger } from '../../server/lib/audit_logger'; -import mappings from './mappings.json'; import { wrapError } from './server/lib/errors'; -import { migrateToKibana660 } from './server/lib/migrations'; // @ts-ignore import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize'; import { initEnterSpaceView } from './server/routes/views'; @@ -39,18 +37,6 @@ export const spaces = (kibana: Record) => managementSections: [], apps: [], hacks: ['plugins/spaces/legacy'], - mappings, - migrations: { - space: { - '6.6.0': migrateToKibana660, - }, - }, - savedObjectSchemas: { - space: { - isNamespaceAgnostic: true, - hidden: true, - }, - }, home: [], injectDefaultVars(server: Server) { return { @@ -100,7 +86,6 @@ export const spaces = (kibana: Record) => const { registerLegacyAPI, createDefaultSpace } = spacesPlugin.__legacyCompat; registerLegacyAPI({ - savedObjects: server.savedObjects, auditLogger: { create: (pluginId: string) => new AuditLogger(server, pluginId, server.config(), server.plugins.xpack_main.info), diff --git a/x-pack/legacy/plugins/spaces/mappings.json b/x-pack/legacy/plugins/spaces/mappings.json deleted file mode 100644 index dc73dc2871885..0000000000000 --- a/x-pack/legacy/plugins/spaces/mappings.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "space": { - "properties": { - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - }, - "description": { - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "color": { - "type": "keyword" - }, - "disabledFeatures": { - "type": "keyword" - }, - "imageUrl": { - "type": "text", - "index": false - }, - "_reserved": { - "type": "boolean" - } - } - } -} diff --git a/x-pack/legacy/plugins/spaces/server/lib/migrations/migrate_6x.test.ts b/x-pack/legacy/plugins/spaces/server/lib/migrations/migrate_6x.test.ts deleted file mode 100644 index 964eb8137685f..0000000000000 --- a/x-pack/legacy/plugins/spaces/server/lib/migrations/migrate_6x.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { migrateToKibana660 } from './migrate_6x'; - -describe('migrateTo660', () => { - it('adds a "disabledFeatures" attribute initialized as an empty array', () => { - expect( - migrateToKibana660({ - id: 'space:foo', - attributes: {}, - }) - ).toEqual({ - id: 'space:foo', - attributes: { - disabledFeatures: [], - }, - }); - }); - - it('does not initialize "disabledFeatures" if the property already exists', () => { - // This scenario shouldn't happen organically. Protecting against defects in the migration. - expect( - migrateToKibana660({ - id: 'space:foo', - attributes: { - disabledFeatures: ['foo', 'bar', 'baz'], - }, - }) - ).toEqual({ - id: 'space:foo', - attributes: { - disabledFeatures: ['foo', 'bar', 'baz'], - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/common/constants/index.ts b/x-pack/legacy/plugins/uptime/common/constants/index.ts index 19f2de3c6f0f4..74783cf46550f 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/index.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/index.ts @@ -8,7 +8,6 @@ export { ACTION_GROUP_DEFINITIONS } from './alerts'; export { CHART_FORMAT_LIMITS } from './chart_format_limits'; export { CLIENT_DEFAULTS } from './client_defaults'; export { CONTEXT_DEFAULTS } from './context_defaults'; -export { INDEX_NAMES } from './index_names'; export * from './capabilities'; export { PLUGIN } from './plugin'; export { QUERY, STATES } from './query'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx index 89227cdd56457..85d0b1b593704 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx @@ -9,7 +9,12 @@ import uuid from 'uuid'; import styled from 'styled-components'; import { npStart } from 'ui/new_platform'; -import { ViewMode } from '../../../../../../../../../src/plugins/embeddable/public'; +import { + ViewMode, + EmbeddableOutput, + ErrorEmbeddable, + isErrorEmbeddable, +} from '../../../../../../../../../src/plugins/embeddable/public'; import * as i18n from './translations'; import { MapEmbeddable, MapEmbeddableInput } from '../../../../../../maps/public'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../../../plugins/maps/public'; @@ -45,9 +50,13 @@ const EmbeddedPanel = styled.div` export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProps) => { const { colors } = useContext(UptimeThemeContext); - const [embeddable, setEmbeddable] = useState(); + const [embeddable, setEmbeddable] = useState(); const embeddableRoot: React.RefObject = useRef(null); - const factory = npStart.plugins.embeddable.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); + const factory = npStart.plugins.embeddable.getEmbeddableFactory< + MapEmbeddableInput, + EmbeddableOutput, + MapEmbeddable + >(MAP_SAVED_OBJECT_TYPE); const input: MapEmbeddableInput = { id: uuid.v4(), @@ -76,12 +85,17 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp useEffect(() => { async function setupEmbeddable() { - const mapState = { - layerList: getLayerList(upPoints, downPoints, colors), + if (!factory) { + throw new Error('Map embeddable not found.'); + } + const embeddableObject = await factory.create({ + ...input, title: i18n.MAP_TITLE, - }; - // @ts-ignore - const embeddableObject = await factory.createFromState(mapState, input, undefined); + }); + + if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { + embeddableObject.setLayerList(getLayerList(upPoints, downPoints, colors)); + } setEmbeddable(embeddableObject); } @@ -93,7 +107,7 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp // update map layers based on points useEffect(() => { - if (embeddable) { + if (embeddable && !isErrorEmbeddable(embeddable)) { embeddable.setLayerList(getLayerList(upPoints, downPoints, colors)); } }, [upPoints, downPoints, embeddable, colors]); diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap index 85988af04a939..9957f13fc1334 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap @@ -3,7 +3,7 @@ exports[`ML JobLink renders without errors 1`] = ` diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout_container.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout_container.tsx index fad12de94fd81..9eed24e2810d8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout_container.tsx @@ -12,6 +12,7 @@ import { hasMLJobSelector, hasNewMLJobSelector, isMLJobCreatingSelector, + selectDynamicSettings, } from '../../../state/selectors'; import { createMLJobAction, getExistingMLJobAction } from '../../../state/actions'; import { MLJobLink } from './ml_job_link'; @@ -24,6 +25,7 @@ import { MLFlyoutView } from './ml_flyout'; import { ML_JOB_ID } from '../../../../common/constants'; import { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts'; import { useUrlParams } from '../../../hooks'; +import { getDynamicSettings } from '../../../state/actions/dynamic_settings'; interface Props { onClose: () => void; @@ -48,13 +50,13 @@ const showMLJobNotification = (

), - toastLifeTimeMs: 5000, + toastLifeTimeMs: 10000, }); } else { - notifications.toasts.warning({ + notifications.toasts.danger({ title:

{labels.JOB_CREATION_FAILED}

, body: message ??

{labels.JOB_CREATION_FAILED_MESSAGE}

, - toastLifeTimeMs: 5000, + toastLifeTimeMs: 10000, }); } }; @@ -65,6 +67,12 @@ export const MachineLearningFlyout: React.FC = ({ onClose }) => { const dispatch = useDispatch(); const { data: hasMLJob, error } = useSelector(hasNewMLJobSelector); const isMLJobCreating = useSelector(isMLJobCreatingSelector); + const { settings } = useSelector(selectDynamicSettings); + useEffect(() => { + // Attempt to load or refresh the dynamic settings + dispatch(getDynamicSettings({})); + }, [dispatch]); + const heartbeatIndices = settings?.heartbeatIndices || ''; const { basePath } = useContext(UptimeSettingsContext); const { refreshApp } = useContext(UptimeRefreshContext); @@ -72,9 +80,12 @@ export const MachineLearningFlyout: React.FC = ({ onClose }) => { let { monitorId } = useParams(); monitorId = atob(monitorId || ''); - const createMLJob = () => dispatch(createMLJobAction.get({ monitorId: monitorId as string })); + const canCreateMLJob = useSelector(canCreateMLJobSelector) && heartbeatIndices !== ''; - const canCreateMLJob = useSelector(canCreateMLJobSelector); + // This function is a noop in the form's disabled state + const createMLJob = heartbeatIndices + ? () => dispatch(createMLJobAction.get({ monitorId: monitorId as string, heartbeatIndices })) + : () => null; const { data: uptimeJobs } = useSelector(hasMLJobSelector); @@ -108,7 +119,7 @@ export const MachineLearningFlyout: React.FC = ({ onClose }) => { basePath, { to: dateRangeEnd, from: dateRangeStart }, false, - error?.body?.message + error?.message || error?.body?.message ); } setIsCreatingJob(false); @@ -130,9 +141,9 @@ export const MachineLearningFlyout: React.FC = ({ onClose }) => { useEffect(() => { if (hasExistingMLJob) { setIsCreatingJob(true); - dispatch(createMLJobAction.get({ monitorId: monitorId as string })); + dispatch(createMLJobAction.get({ monitorId: monitorId as string, heartbeatIndices })); } - }, [dispatch, hasExistingMLJob, monitorId]); + }, [dispatch, hasExistingMLJob, heartbeatIndices, monitorId]); if (hasExistingMLJob) { return null; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts index 9a8e4036f2cff..2e83490b71b54 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts @@ -8,7 +8,12 @@ import { createAction } from 'redux-actions'; import { createAsyncAction } from './utils'; import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges'; import { AnomaliesTableRecord } from '../../../../../../plugins/ml/common/types/anomalies'; -import { CreateMLJobSuccess, DeleteJobResults, MonitorIdParam } from './types'; +import { + CreateMLJobSuccess, + DeleteJobResults, + MonitorIdParam, + HeartbeatIndicesParam, +} from './types'; import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer'; export const resetMLState = createAction('RESET_ML_STATE'); @@ -17,9 +22,10 @@ export const getExistingMLJobAction = createAsyncAction( - 'CREATE_ML_JOB' -); +export const createMLJobAction = createAsyncAction< + MonitorIdParam & HeartbeatIndicesParam, + CreateMLJobSuccess | null +>('CREATE_ML_JOB'); export const getMLCapabilitiesAction = createAsyncAction( 'GET_ML_CAPABILITIES' diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/types.ts b/x-pack/legacy/plugins/uptime/public/state/actions/types.ts index 236b263414a26..41381afd31453 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/types.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/types.ts @@ -22,6 +22,10 @@ export interface MonitorIdParam { monitorId: string; } +export interface HeartbeatIndicesParam { + heartbeatIndices: string; +} + export interface QueryParams { monitorId: string; dateStart: string; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts index 80b97783769b5..bcd2582fe18b9 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts @@ -7,13 +7,18 @@ import moment from 'moment'; import { apiService } from './utils'; import { AnomalyRecords, AnomalyRecordsParams } from '../actions'; -import { API_URLS, INDEX_NAMES, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants'; +import { API_URLS, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants'; import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges'; -import { CreateMLJobSuccess, DeleteJobResults, MonitorIdParam } from '../actions/types'; +import { + CreateMLJobSuccess, + DeleteJobResults, + MonitorIdParam, + HeartbeatIndicesParam, +} from '../actions/types'; import { DataRecognizerConfigResponse } from '../../../../../../plugins/ml/common/types/modules'; import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer'; -export const getMLJobId = (monitorId: string) => `${monitorId}_${ML_JOB_ID}`; +export const getMLJobId = (monitorId: string) => `${monitorId}_${ML_JOB_ID}`.toLowerCase(); export const getMLCapabilities = async (): Promise => { return await apiService.get(API_URLS.ML_CAPABILITIES); @@ -25,23 +30,27 @@ export const getExistingJobs = async (): Promise => { export const createMLJob = async ({ monitorId, -}: MonitorIdParam): Promise => { + heartbeatIndices, +}: MonitorIdParam & HeartbeatIndicesParam): Promise => { const url = API_URLS.ML_SETUP_MODULE + ML_MODULE_ID; + // ML App doesn't support upper case characters in job name + const lowerCaseMonitorId = monitorId.toLowerCase(); + const data = { - prefix: `${monitorId}_`, + prefix: `${lowerCaseMonitorId}_`, useDedicatedIndex: false, startDatafeed: true, start: moment() .subtract(24, 'h') .valueOf(), - indexPatternName: INDEX_NAMES.HEARTBEAT, + indexPatternName: heartbeatIndices, query: { bool: { filter: [ { term: { - 'monitor.id': monitorId, + 'monitor.id': lowerCaseMonitorId, }, }, ], @@ -50,11 +59,17 @@ export const createMLJob = async ({ }; const response: DataRecognizerConfigResponse = await apiService.post(url, data); - if (response?.jobs?.[0]?.id === getMLJobId(monitorId) && response?.jobs?.[0]?.success) { - return { - count: 1, - jobId: response?.jobs?.[0]?.id, - }; + if (response?.jobs?.[0]?.id === getMLJobId(monitorId)) { + const jobResponse = response.jobs[0]; + if (jobResponse.success) { + return { + count: 1, + jobId: jobResponse.id, + }; + } else { + const { error } = jobResponse; + throw new Error(error?.msg); + } } else { return null; } diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js index 4cf32b971b57e..6ce457ffbec05 100644 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ b/x-pack/legacy/plugins/xpack_main/index.js @@ -23,11 +23,6 @@ export const xpackMain = kibana => { config(Joi) { return Joi.object({ enabled: Joi.boolean().default(true), - telemetry: Joi.object({ - config: Joi.string().default(), - enabled: Joi.boolean().default(), - url: Joi.string().default(), - }).default(), // deprecated }).default(); }, diff --git a/x-pack/package.json b/x-pack/package.json index bbab1a96f52f4..24b23256bf18e 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -315,6 +315,7 @@ "react-portal": "^3.2.0", "react-redux": "^7.1.3", "react-reverse-portal": "^1.0.4", + "react-router": "^5.1.2", "react-router-dom": "^5.1.2", "react-shortcuts": "^2.0.0", "react-sticky": "^6.0.3", diff --git a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts index 91b829a020048..10860fe471a3b 100644 --- a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts +++ b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts @@ -10,9 +10,7 @@ import { skip } from 'rxjs/operators'; import * as Rx from 'rxjs'; import { mount } from 'enzyme'; -import { EmbeddableFactory } from '../../../../src/plugins/embeddable/public'; import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers'; -import { TimeRangeEmbeddableFactory } from './test_helpers/time_range_embeddable_factory'; import { CustomTimeRangeAction } from './custom_time_range_action'; /* eslint-disable */ import { @@ -21,7 +19,6 @@ import { /* eslint-enable */ import { - HelloWorldEmbeddableFactory, HelloWorldEmbeddable, HELLO_WORLD_EMBEDDABLE, } from '../../../../examples/embeddable_examples/public'; @@ -38,9 +35,6 @@ const createOpenModalMock = () => { }; test('Custom time range action prevents embeddable from using container time', async done => { - const embeddableFactories = new Map(); - embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); - const container = new TimeRangeContainer( { timeRange: { from: 'now-15m', to: 'now' }, @@ -105,9 +99,6 @@ test('Custom time range action prevents embeddable from using container time', a }); test('Removing custom time range action resets embeddable back to container time', async done => { - const embeddableFactories = new Map(); - embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); - const container = new TimeRangeContainer( { timeRange: { from: 'now-15m', to: 'now' }, @@ -182,9 +173,6 @@ test('Removing custom time range action resets embeddable back to container time }); test('Cancelling custom time range action leaves state alone', async done => { - const embeddableFactories = new Map(); - embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); - const container = new TimeRangeContainer( { timeRange: { from: 'now-15m', to: 'now' }, @@ -244,8 +232,6 @@ test('Cancelling custom time range action leaves state alone', async done => { }); test(`badge is compatible with embeddable that inherits from parent`, async () => { - const embeddableFactories = new Map(); - embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); const container = new TimeRangeContainer( { timeRange: { from: 'now-15m', to: 'now' }, @@ -279,8 +265,6 @@ test(`badge is compatible with embeddable that inherits from parent`, async () = // TODO: uncomment when https://github.com/elastic/kibana/issues/43271 is fixed. // test('Embeddable that does not use time range in a container that has time range is incompatible', async () => { -// const embeddableFactories = new Map(); -// embeddableFactories.set(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory()); // const container = new TimeRangeContainer( // { // timeRange: { from: 'now-15m', to: 'now' }, @@ -315,8 +299,6 @@ test(`badge is compatible with embeddable that inherits from parent`, async () = // }); test('Attempting to execute on incompatible embeddable throws an error', async () => { - const embeddableFactories = new Map(); - embeddableFactories.set(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory()); const container = new HelloWorldContainer( { panels: { diff --git a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.test.ts b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.test.ts index d2b9fa9ac1655..3bf763470f002 100644 --- a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.test.ts +++ b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.test.ts @@ -9,17 +9,12 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { skip } from 'rxjs/operators'; import * as Rx from 'rxjs'; import { mount } from 'enzyme'; -import { EmbeddableFactory } from '../../../../src/plugins/embeddable/public'; import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers'; -import { TimeRangeEmbeddableFactory } from './test_helpers/time_range_embeddable_factory'; import { CustomTimeRangeBadge } from './custom_time_range_badge'; import { ReactElement } from 'react'; import { nextTick } from 'test_utils/enzyme_helpers'; test('Removing custom time range from badge resets embeddable back to container time', async done => { - const embeddableFactories = new Map(); - embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); - const container = new TimeRangeContainer( { timeRange: { from: 'now-15m', to: 'now' }, @@ -79,8 +74,6 @@ test('Removing custom time range from badge resets embeddable back to container }); test(`badge is not compatible with embeddable that inherits from parent`, async () => { - const embeddableFactories = new Map(); - embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); const container = new TimeRangeContainer( { timeRange: { from: 'now-15m', to: 'now' }, @@ -113,8 +106,6 @@ test(`badge is not compatible with embeddable that inherits from parent`, async }); test(`badge is compatible with embeddable that has custom time range`, async () => { - const embeddableFactories = new Map(); - embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); const container = new TimeRangeContainer( { timeRange: { from: 'now-15m', to: 'now' }, @@ -148,8 +139,6 @@ test(`badge is compatible with embeddable that has custom time range`, async () }); test('Attempting to execute on incompatible embeddable throws an error', async () => { - const embeddableFactories = new Map(); - embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory()); const container = new TimeRangeContainer( { timeRange: { from: 'now-15m', to: 'now' }, diff --git a/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts index 311d3357476b9..e8d9451f9f2a6 100644 --- a/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts +++ b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts @@ -7,7 +7,7 @@ import { EmbeddableInput, IContainer, - EmbeddableFactory, + EmbeddableFactoryDefinition, } from '../../../../../src/plugins/embeddable/public'; import { TimeRange } from '../../../../../src/plugins/data/public'; import { TIME_RANGE_EMBEDDABLE, TimeRangeEmbeddable } from './time_range_embeddable'; @@ -16,7 +16,8 @@ interface EmbeddableTimeRangeInput extends EmbeddableInput { timeRange: TimeRange; } -export class TimeRangeEmbeddableFactory extends EmbeddableFactory { +export class TimeRangeEmbeddableFactory + implements EmbeddableFactoryDefinition { public readonly type = TIME_RANGE_EMBEDDABLE; public async isEditable() { diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap index 81adf76ac4ce9..4b74b07fc8e27 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap @@ -167,12 +167,6 @@ Array [ "validationError": "Must be a number between 0.000 and 1", "validationName": "numberFloatRt", }, - Object { - "key": "trace_methods_duration_threshold", - "type": "integer", - "validationError": "Must be an integer", - "validationName": "integerRt", - }, Object { "key": "transaction_max_spans", "max": 32000, diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index cfe4aa01a4a99..152db37a1bff3 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -28,7 +28,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'The maximum total compressed size of the request body which is sent to the APM Server intake api via a chunked encoding (HTTP streaming).\nNote that a small overshoot is possible.\n\nAllowed byte units are `b`, `kb` and `mb`. `1kb` is equal to `1024b`.' } ), - excludeAgents: ['js-base', 'rum-js', 'dotnet'] + excludeAgents: ['js-base', 'rum-js', 'dotnet', 'go', 'nodejs'] }, // API Request Time @@ -46,7 +46,7 @@ export const generalSettings: RawSettingDefinition[] = [ "Maximum time to keep an HTTP request to the APM Server open for.\n\nNOTE: This value has to be lower than the APM Server's `read_timeout` setting." } ), - excludeAgents: ['js-base', 'rum-js', 'dotnet'] + excludeAgents: ['js-base', 'rum-js', 'dotnet', 'go', 'nodejs'] }, // Capture body @@ -89,7 +89,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'If set to `true`, the agent will capture request and response headers, including cookies.\n\nNOTE: Setting this to `false` reduces network bandwidth, disk space and object allocations.' } ), - excludeAgents: ['js-base', 'rum-js'] + excludeAgents: ['js-base', 'rum-js', 'nodejs'] }, // LOG_LEVEL @@ -103,7 +103,7 @@ export const generalSettings: RawSettingDefinition[] = [ description: i18n.translate('xpack.apm.agentConfig.logLevel.description', { defaultMessage: 'Sets the logging level for the agent' }), - excludeAgents: ['js-base', 'rum-js', 'python'] + includeAgents: ['dotnet', 'ruby'] }, // Recording @@ -117,7 +117,8 @@ export const generalSettings: RawSettingDefinition[] = [ description: i18n.translate('xpack.apm.agentConfig.recording.description', { defaultMessage: 'When recording, the agent instruments incoming HTTP requests, tracks errors, and collects and sends metrics. When inactive, the agent works as a noop, not collecting data and not communicating with the APM Server except for polling for updated configuration. As this is a reversible switch, agent threads are not being killed when inactivated, but they will be mostly idle in this state, so the overhead should be negligible. You can use this setting to dynamically control whether Elastic APM is enabled or disabled.' - }) + }), + excludeAgents: ['nodejs'] }, // SERVER_TIMEOUT @@ -135,7 +136,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'If a request to the APM Server takes longer than the configured timeout,\nthe request is cancelled and the event (exception or transaction) is discarded.\nSet to 0 to disable timeouts.\n\nWARNING: If timeouts are disabled or set to a high value, your app could experience memory issues if the APM Server times out.' } ), - includeAgents: ['nodejs', 'java', 'go'] + includeAgents: ['java'] }, // SPAN_FRAMES_MIN_DURATION @@ -171,7 +172,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'Setting it to 0 will disable stack trace collection. Any positive integer value will be used as the maximum number of frames to collect. Setting it -1 means that all frames will be collected.' } ), - includeAgents: ['nodejs', 'java', 'dotnet', 'go'] + includeAgents: ['java', 'dotnet', 'go'] }, // Transaction max spans diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts index b0255d2d828bb..7fa44b8c85f41 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts @@ -43,13 +43,9 @@ describe('filterByAgent', () => { describe('options per agent', () => { it('go', () => { expect(getSettingKeysForAgent('go')).toEqual([ - 'api_request_size', - 'api_request_time', 'capture_body', 'capture_headers', - 'log_level', 'recording', - 'server_timeout', 'span_frames_min_duration', 'stack_trace_limit', 'transaction_max_spans', @@ -65,7 +61,6 @@ describe('filterByAgent', () => { 'capture_headers', 'circuit_breaker_enabled', 'enable_log_correlation', - 'log_level', 'profiling_inferred_spans_enabled', 'profiling_inferred_spans_excluded_classes', 'profiling_inferred_spans_included_classes', @@ -80,7 +75,6 @@ describe('filterByAgent', () => { 'stress_monitor_gc_stress_threshold', 'stress_monitor_system_cpu_relief_threshold', 'stress_monitor_system_cpu_stress_threshold', - 'trace_methods_duration_threshold', 'transaction_max_spans', 'transaction_sample_rate' ]); @@ -102,14 +96,7 @@ describe('filterByAgent', () => { it('nodejs', () => { expect(getSettingKeysForAgent('nodejs')).toEqual([ - 'api_request_size', - 'api_request_time', 'capture_body', - 'capture_headers', - 'log_level', - 'recording', - 'server_timeout', - 'stack_trace_limit', 'transaction_max_spans', 'transaction_sample_rate' ]); @@ -158,8 +145,6 @@ describe('filterByAgent', () => { it('"All" services (no agent name)', () => { expect(getSettingKeysForAgent(undefined)).toEqual([ 'capture_body', - 'capture_headers', - 'recording', 'transaction_max_spans', 'transaction_sample_rate' ]); diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts index 7331b6c5dcbf5..bb050076b9f9a 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts @@ -26,26 +26,6 @@ export const javaSettings: RawSettingDefinition[] = [ includeAgents: ['java'] }, - // TRACE_METHODS_DURATION_THRESHOLD - { - key: 'trace_methods_duration_threshold', - type: 'integer', - label: i18n.translate( - 'xpack.apm.agentConfig.traceMethodsDurationThreshold.label', - { - defaultMessage: 'Trace methods duration threshold' - } - ), - description: i18n.translate( - 'xpack.apm.agentConfig.traceMethodsDurationThreshold.description', - { - defaultMessage: - 'If trace_methods config option is set, provides a threshold to limit spans based on duration. When set to a value greater than 0, spans representing methods traced based on trace_methods will be discarded by default.' - } - ), - includeAgents: ['java'] - }, - /* * Circuit-Breaker **/ diff --git a/x-pack/plugins/apm/common/service_map.test.ts b/x-pack/plugins/apm/common/service_map.test.ts new file mode 100644 index 0000000000000..40b220ffe68f1 --- /dev/null +++ b/x-pack/plugins/apm/common/service_map.test.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { License } from '../../licensing/common/license'; +import * as serviceMap from './service_map'; + +describe('service map helpers', () => { + describe('isValidPlatinumLicense', () => { + describe('with an expired license', () => { + it('returns false', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'platinum', + type: 'platinum', + status: 'expired' + }, + signature: 'test signature' + }); + + expect(serviceMap.isValidPlatinumLicense(license)).toEqual(false); + }); + }); + + describe('with a basic license', () => { + it('returns false', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'basic', + type: 'basic', + status: 'active' + }, + signature: 'test signature' + }); + + expect(serviceMap.isValidPlatinumLicense(license)).toEqual(false); + }); + }); + + describe('with a platinum license', () => { + it('returns true', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'platinum', + type: 'platinum', + status: 'active' + }, + signature: 'test signature' + }); + + expect(serviceMap.isValidPlatinumLicense(license)).toEqual(true); + }); + }); + + describe('with an enterprise license', () => { + it('returns true', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'enterprise', + type: 'enterprise', + status: 'active' + }, + signature: 'test signature' + }); + + expect(serviceMap.isValidPlatinumLicense(license)).toEqual(true); + }); + }); + + describe('with a trial license', () => { + it('returns true', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'trial', + type: 'trial', + status: 'active' + }, + signature: 'test signature' + }); + + expect(serviceMap.isValidPlatinumLicense(license)).toEqual(true); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 4a8608199c037..75c1c945c5d26 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -45,10 +45,7 @@ export interface ServiceNodeMetrics { } export function isValidPlatinumLicense(license: ILicense) { - return ( - license.isActive && - (license.type === 'platinum' || license.type === 'trial') - ); + return license.isActive && license.hasAtLeast('platinum'); } export const invalidLicenseMessage = i18n.translate( diff --git a/x-pack/plugins/endpoint/common/generate_data.test.ts b/x-pack/plugins/endpoint/common/generate_data.test.ts index dfb906c7af606..88e1c66ea3e82 100644 --- a/x-pack/plugins/endpoint/common/generate_data.test.ts +++ b/x-pack/plugins/endpoint/common/generate_data.test.ts @@ -86,7 +86,7 @@ describe('data generator', () => { let events: Event[]; beforeEach(() => { - events = generator.generateAlertEventAncestry(3); + events = generator.createAlertEventAncestry(3); }); it('with n-1 process events', () => { @@ -153,7 +153,7 @@ describe('data generator', () => { const timestamp = new Date().getTime(); const root = generator.generateEvent({ timestamp }); const generations = 2; - const events = [root, ...generator.generateDescendantsTree(root, generations)]; + const events = [root, ...generator.descendantsTreeGenerator(root, generations)]; const rootNode = buildResolverTree(events); const visitedEvents = countResolverEvents(rootNode, generations); expect(visitedEvents).toEqual(events.length); @@ -162,7 +162,7 @@ describe('data generator', () => { it('creates full resolver tree', () => { const alertAncestors = 3; const generations = 2; - const events = generator.generateFullResolverTree(alertAncestors, generations); + const events = [...generator.fullResolverTreeGenerator(alertAncestors, generations)]; const rootNode = buildResolverTree(events); const visitedEvents = countResolverEvents(rootNode, alertAncestors + generations); expect(visitedEvents).toEqual(events.length); diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index 75351bb3bf07d..0ec105129b7ac 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -83,6 +83,11 @@ const OTHER_EVENT_CATEGORIES: EventInfo[] = [ ]; interface HostInfo { + elastic: { + agent: { + id: string; + }; + }; agent: { version: string; id: string; @@ -95,19 +100,30 @@ interface HostInfo { }; } +interface NodeState { + event: Event; + childrenCreated: number; + maxChildren: number; +} + export class EndpointDocGenerator { commonInfo: HostInfo; random: seedrandom.prng; - constructor(seed = Math.random().toString()) { - this.random = seedrandom(seed); + constructor(seed: string | seedrandom.prng = Math.random().toString()) { + if (typeof seed === 'string') { + this.random = seedrandom(seed); + } else { + this.random = seed; + } this.commonInfo = this.createHostData(); } - // This function will create new values for all the host fields, so documents from a different host can be created - // This provides a convenient way to make documents from multiple hosts that are all tied to a single seed value - public randomizeHostData() { - this.commonInfo = this.createHostData(); + /** + * Creates new random IP addresses for the host to simulate new DHCP assignment + */ + public updateHostData() { + this.commonInfo.host.ip = this.randomArray(3, () => this.randomIP()); } private createHostData(): HostInfo { @@ -116,6 +132,11 @@ export class EndpointDocGenerator { version: this.randomVersion(), id: this.seededUUIDv4(), }, + elastic: { + agent: { + id: this.seededUUIDv4(), + }, + }, host: { id: this.seededUUIDv4(), hostname: this.randomHostname(), @@ -129,6 +150,10 @@ export class EndpointDocGenerator { }; } + /** + * Creates a host metadata document + * @param ts - Timestamp to put in the event + */ public generateHostMetadata(ts = new Date().getTime()): HostMetadata { return { '@timestamp': ts, @@ -139,6 +164,12 @@ export class EndpointDocGenerator { }; } + /** + * Creates an alert from the simulated host represented by this EndpointDocGenerator + * @param ts - Timestamp to put in the event + * @param entityID - entityID of the originating process + * @param parentEntityID - optional entityID of the parent process, if it exists + */ public generateAlert( ts = new Date().getTime(), entityID = this.randomString(10), @@ -173,7 +204,7 @@ export class EndpointDocGenerator { trusted: false, subject_name: 'bad signer', }, - malware_classifier: { + malware_classification: { identifier: 'endpointpe', score: 1, threshold: 0.66, @@ -231,7 +262,7 @@ export class EndpointDocGenerator { sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', }, - malware_classifier: { + malware_classification: { identifier: 'Whitelisted', score: 0, threshold: 0, @@ -245,6 +276,10 @@ export class EndpointDocGenerator { }; } + /** + * Creates an event, customized by the options parameter + * @param options - Allows event field values to be specified + */ public generateEvent(options: EventOptions = {}): EndpointEvent { return { '@timestamp': options.timestamp ? options.timestamp : new Date().getTime(), @@ -267,17 +302,31 @@ export class EndpointDocGenerator { }; } - public generateFullResolverTree( + /** + * Generator function that creates the full set of events needed to render resolver. + * The number of nodes grows exponentially with the number of generations and children per node. + * Each node is logically a process, and will have 1 or more process events associated with it. + * @param alertAncestors - number of ancestor generations to create relative to the alert + * @param childGenerations - number of child generations to create relative to the alert + * @param maxChildrenPerNode - maximum number of children for any given node in the tree + * @param relatedEventsPerNode - number of related events (file, registry, etc) to create for each process event in the tree + * @param percentNodesWithRelated - percent of nodes which should have related events + * @param percentChildrenTerminated - percent of nodes which will have process termination events + */ + public *fullResolverTreeGenerator( alertAncestors?: number, childGenerations?: number, maxChildrenPerNode?: number, relatedEventsPerNode?: number, percentNodesWithRelated?: number, percentChildrenTerminated?: number - ): Event[] { - const ancestry = this.generateAlertEventAncestry(alertAncestors); + ) { + const ancestry = this.createAlertEventAncestry(alertAncestors); + for (let i = 0; i < ancestry.length; i++) { + yield ancestry[i]; + } // ancestry will always have at least 2 elements, and the second to last element will be the process associated with the alert - const descendants = this.generateDescendantsTree( + yield* this.descendantsTreeGenerator( ancestry[ancestry.length - 2], childGenerations, maxChildrenPerNode, @@ -285,10 +334,13 @@ export class EndpointDocGenerator { percentNodesWithRelated, percentChildrenTerminated ); - return ancestry.concat(descendants); } - public generateAlertEventAncestry(alertAncestors = 3): Event[] { + /** + * Creates an alert event and associated process ancestry. The alert event will always be the last event in the return array. + * @param alertAncestors - number of ancestor generations to create + */ + public createAlertEventAncestry(alertAncestors = 3): Event[] { const events = []; const startDate = new Date().getTime(); const root = this.generateEvent({ timestamp: startDate + 1000 }); @@ -311,75 +363,93 @@ export class EndpointDocGenerator { return events; } - public generateDescendantsTree( + /** + * Creates the child generations of a process. The number of returned events grows exponentially with generations and maxChildrenPerNode. + * @param root - The process event to use as the root node of the tree + * @param generations - number of child generations to create. The root node is not counted as a generation. + * @param maxChildrenPerNode - maximum number of children for any given node in the tree + * @param relatedEventsPerNode - number of related events (file, registry, etc) to create for each process event in the tree + * @param percentNodesWithRelated - percent of nodes which should have related events + * @param percentChildrenTerminated - percent of nodes which will have process termination events + */ + public *descendantsTreeGenerator( root: Event, generations = 2, maxChildrenPerNode = 2, relatedEventsPerNode = 3, percentNodesWithRelated = 100, percentChildrenTerminated = 100 - ): Event[] { - let events: Event[] = []; - let parents = [root]; + ) { + const rootState: NodeState = { + event: root, + childrenCreated: 0, + maxChildren: this.randomN(maxChildrenPerNode + 1), + }; + const lineage: NodeState[] = [rootState]; let timestamp = root['@timestamp']; - for (let i = 0; i < generations; i++) { - const newParents: EndpointEvent[] = []; - parents.forEach(element => { - const numChildren = this.randomN(maxChildrenPerNode + 1); - for (let j = 0; j < numChildren; j++) { - timestamp = timestamp + 1000; - const child = this.generateEvent({ - timestamp, - parentEntityID: element.process.entity_id, - }); - newParents.push(child); - } + while (lineage.length > 0) { + const currentState = lineage[lineage.length - 1]; + // If we get to a state node and it has made all the children, move back up a level + if ( + currentState.childrenCreated === currentState.maxChildren || + lineage.length === generations + 1 + ) { + lineage.pop(); + continue; + } + // Otherwise, add a child and any nodes associated with it + currentState.childrenCreated++; + timestamp = timestamp + 1000; + const child = this.generateEvent({ + timestamp, + parentEntityID: currentState.event.process.entity_id, }); - events = events.concat(newParents); - parents = newParents; - } - const terminationEvents: EndpointEvent[] = []; - let relatedEvents: EndpointEvent[] = []; - events.forEach(element => { + lineage.push({ + event: child, + childrenCreated: 0, + maxChildren: this.randomN(maxChildrenPerNode + 1), + }); + yield child; + let processDuration: number = 6 * 3600; if (this.randomN(100) < percentChildrenTerminated) { - timestamp = timestamp + 1000; - terminationEvents.push( - this.generateEvent({ - timestamp, - entityID: element.process.entity_id, - parentEntityID: element.process.parent?.entity_id, - eventCategory: 'process', - eventType: 'end', - }) - ); + processDuration = this.randomN(1000000); // This lets termination events be up to 1 million seconds after the creation event (~11 days) + yield this.generateEvent({ + timestamp: timestamp + processDuration * 1000, + entityID: child.process.entity_id, + parentEntityID: child.process.parent?.entity_id, + eventCategory: 'process', + eventType: 'end', + }); } if (this.randomN(100) < percentNodesWithRelated) { - relatedEvents = relatedEvents.concat( - this.generateRelatedEvents(element, relatedEventsPerNode) - ); + yield* this.relatedEventsGenerator(child, relatedEventsPerNode, processDuration); } - }); - events = events.concat(terminationEvents); - events = events.concat(relatedEvents); - return events; + } } - public generateRelatedEvents(node: Event, numRelatedEvents = 10): EndpointEvent[] { - const ts = node['@timestamp'] + 1000; - const relatedEvents: EndpointEvent[] = []; + /** + * Creates related events for a process event + * @param node - process event to relate events to by entityID + * @param numRelatedEvents - number of related events to generate + * @param processDuration - maximum number of seconds after process event that related event timestamp can be + */ + public *relatedEventsGenerator( + node: Event, + numRelatedEvents = 10, + processDuration: number = 6 * 3600 + ) { for (let i = 0; i < numRelatedEvents; i++) { const eventInfo = this.randomChoice(OTHER_EVENT_CATEGORIES); - relatedEvents.push( - this.generateEvent({ - timestamp: ts, - entityID: node.process.entity_id, - parentEntityID: node.process.parent?.entity_id, - eventCategory: eventInfo.category, - eventType: eventInfo.creationType, - }) - ); + + const ts = node['@timestamp'] + this.randomN(processDuration) * 1000; + yield this.generateEvent({ + timestamp: ts, + entityID: node.process.entity_id, + parentEntityID: node.process.parent?.entity_id, + eventCategory: eventInfo.category, + eventType: eventInfo.creationType, + }); } - return relatedEvents; } private randomN(n: number): number { diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index b3eb518e35ae3..e8e1281a88925 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -113,7 +113,7 @@ export interface HashFields { sha1: string; sha256: string; } -export interface MalwareClassifierFields { +export interface MalwareClassificationFields { identifier: string; score: number; threshold: number; @@ -142,7 +142,7 @@ export interface DllFields { }; compile_time: number; hash: HashFields; - malware_classifier: MalwareClassifierFields; + malware_classification: MalwareClassificationFields; mapped_address: number; mapped_size: number; path: string; @@ -194,7 +194,7 @@ export type AlertEvent = Immutable<{ executable: string; sid?: string; start: number; - malware_classifier?: MalwareClassifierFields; + malware_classification?: MalwareClassificationFields; token: { domain: string; type: string; @@ -224,7 +224,7 @@ export type AlertEvent = Immutable<{ trusted: boolean; subject_name: string; }; - malware_classifier: MalwareClassifierFields; + malware_classification: MalwareClassificationFields; temp_file_path: string; }; host: HostFields; @@ -257,6 +257,11 @@ export type HostMetadata = Immutable<{ event: { created: number; }; + elastic: { + agent: { + id: string; + }; + }; endpoint: { policy: { id: string; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts index e1ac9defc858e..9ac53f9be609f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PolicyConfig } from '../types'; +import { PolicyConfig, ProtectionModes } from '../types'; /** * Generate a new Policy model. @@ -19,7 +19,7 @@ export const generatePolicy = (): PolicyConfig => { network: true, }, malware: { - mode: 'prevent', + mode: ProtectionModes.prevent, }, logging: { stdout: 'debug', @@ -44,7 +44,7 @@ export const generatePolicy = (): PolicyConfig => { process: true, }, malware: { - mode: 'detect', + mode: ProtectionModes.detect, }, logging: { stdout: 'debug', diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index 4215edb4d6810..d4f6d2457254e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -123,10 +123,8 @@ export interface PolicyConfig { process: boolean; network: boolean; }; - /** malware mode can be detect, prevent or prevent and notify user */ - malware: { - mode: string; - }; + /** malware mode can be off, detect, prevent or prevent and notify user */ + malware: MalwareFields; logging: { stdout: string; file: string; @@ -137,9 +135,7 @@ export interface PolicyConfig { events: { process: boolean; }; - malware: { - mode: string; - }; + malware: MalwareFields; logging: { stdout: string; file: string; @@ -209,6 +205,44 @@ export enum EventingFields { network = 'network', } +/** + * Returns the keys of an object whose values meet a criteria. + * Ex) interface largeNestedObject = { + * a: { + * food: Foods; + * toiletPaper: true; + * }; + * b: { + * food: Foods; + * streamingServices: Streams; + * }; + * c: {}; + * } + * + * type hasFoods = KeysByValueCriteria; + * The above type will be: [a, b] only, and will not include c. + * + */ +export type KeysByValueCriteria = { + [K in keyof O]: O[K] extends Criteria ? K : never; +}[keyof O]; + +/** Returns an array of the policy OSes that have a malware protection field */ + +export type MalwareProtectionOSes = KeysByValueCriteria; +/** Policy: Malware protection fields */ +export interface MalwareFields { + mode: ProtectionModes; +} + +/** Policy protection mode options */ +export enum ProtectionModes { + detect = 'detect', + prevent = 'prevent', + preventNotify = 'preventNotify', + off = 'off', +} + export interface GlobalState { readonly hostList: HostListState; readonly alertList: AlertListState; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx index 0183e9663bb44..79cb61693056c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx @@ -40,7 +40,7 @@ export const GeneralAccordion = memo(({ alertData }: { alertData: Immutable }) => { +export const HostAccordion = memo(({ alertData }: { alertData: Immutable }) => { const columns = useMemo(() => { return [ { - title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostName', { - defaultMessage: 'Host Name', + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostNameCurrent', { + defaultMessage: 'Host Name (Current)', + }), + description: alertData.state.host_metadata.host.hostname, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostNameOriginal', { + defaultMessage: 'Host Name (At time of alert)', }), description: alertData.host.hostname, }, { - title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostIP', { - defaultMessage: 'Host IP', + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostIPCurrent', { + defaultMessage: 'Host IP (Current)', + }), + description: alertData.state.host_metadata.host.ip.join(', '), + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostIPOriginal', { + defaultMessage: 'Host IP (At time of alert)', }), description: alertData.host.ip.join(', '), }, { - title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.status', { - defaultMessage: 'Status', + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.currentStatus', { + defaultMessage: 'Current Status', + }), + description: ( + + {' '} + + + ), + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.osCurrent', { + defaultMessage: 'OS (Current)', }), - description: 'TODO', + description: alertData.state.host_metadata.host.os.name, }, { - title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.os', { - defaultMessage: 'OS', + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.osOriginal', { + defaultMessage: 'OS (At time of alert)', }), description: alertData.host.os.name, }, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx index 4134bc35747d6..538562bfbbc06 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx @@ -51,7 +51,7 @@ export const SourceProcessAccordion = memo(({ alertData }: { alertData: Immutabl title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.malwareScore', { defaultMessage: 'MalwareScore', }), - description: alertData.process.malware_classifier?.score || '-', + description: alertData.process.malware_classification?.score || '-', }, { title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.parentProcessID', { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx index b900a0a35dbf5..6a6936c59a00e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx @@ -183,7 +183,7 @@ export const AlertIndex = memo(() => { } else if (columnId === 'archived') { return null; } else if (columnId === 'malware_score') { - return row.file.malware_classifier.score; + return row.file.malware_classification.score; } return null; }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx index f2c79155f3c23..2dba301bf4537 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx @@ -35,6 +35,7 @@ import { AppAction } from '../../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; +import { MalwareProtections } from './policy_forms/protections/malware'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); @@ -181,6 +182,17 @@ export const PolicyDetails = React.memo(() => { headerLeft={headerLeftContent} headerRight={headerRightContent} > + +

+ +

+
+ + +

= React.memo(({ type, supportedOss, children, id, selectedEventing, totalEventing }) => { + /** Takes a react component to be put on the right corner of the card */ + rightCorner: React.ReactNode; +}> = React.memo(({ type, supportedOss, children, id, rightCorner }) => { const typeTitle = () => { return ( @@ -63,32 +62,11 @@ export const ConfigForm: React.FC<{ {supportedOss.join(', ')} - - - - - + {rightCorner} ); }; - const events = () => { - return ( - -
- -
-
- ); - }; - return ( - {events()} - {children} } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx index e92e22fc97fe6..7bec2c4c742d2 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx @@ -6,6 +6,8 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; import { EventingCheckbox } from './checkbox'; import { OS, EventingFields } from '../../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; @@ -16,6 +18,9 @@ import { import { ConfigForm } from '../config_form'; export const WindowsEventing = React.memo(() => { + const selected = usePolicyDetailsSelector(selectedWindowsEventing); + const total = usePolicyDetailsSelector(totalWindowsEventing); + const checkboxes = useMemo( () => [ { @@ -37,21 +42,43 @@ export const WindowsEventing = React.memo(() => { ); const renderCheckboxes = () => { - return checkboxes.map((item, index) => { - return ( - - ); - }); + return ( + <> + +
+ +
+
+ + {checkboxes.map((item, index) => { + return ( + + ); + })} + + ); }; - const selected = usePolicyDetailsSelector(selectedWindowsEventing); - const total = usePolicyDetailsSelector(totalWindowsEventing); + const collectionsEnabled = () => { + return ( + + + + ); + }; return ( { i18n.translate('xpack.endpoint.policy.details.windows', { defaultMessage: 'Windows' }), ]} id="windowsEventingForm" + rightCorner={collectionsEnabled()} children={renderCheckboxes()} - selectedEventing={selected} - totalEventing={total} /> ); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx new file mode 100644 index 0000000000000..66b22178607b9 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; +import { EuiRadio, EuiSwitch, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { htmlIdGenerator } from '@elastic/eui'; +import { Immutable } from '../../../../../../../common/types'; +import { OS, ProtectionModes, MalwareProtectionOSes } from '../../../../types'; +import { ConfigForm } from '../config_form'; +import { policyConfig } from '../../../../store/policy_details/selectors'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { clone } from '../../../../models/policy_details_config'; + +const ProtectionRadioGroup = styled.div` + display: flex; + .policyDetailsProtectionRadio { + margin-right: ${props => props.theme.eui.euiSizeXXL}; + } +`; + +const OSes: Immutable = [OS.windows, OS.mac]; +const protection = 'malware'; + +const ProtectionRadio = React.memo(({ id, label }: { id: ProtectionModes; label: string }) => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch(); + // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value + const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; + + const handleRadioChange = useCallback(() => { + if (policyDetailsConfig) { + const newPayload = clone(policyDetailsConfig); + for (const os of OSes) { + newPayload[os][protection].mode = id; + } + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, [dispatch, id, policyDetailsConfig]); + + /** + * Passing an arbitrary id because EuiRadio + * requires an id if label is passed + */ + + return ( + htmlIdGenerator()(), [])} + checked={selected === id} + onChange={handleRadioChange} + disabled={selected === ProtectionModes.off} + /> + ); +}); + +/** The Malware Protections form for policy details + * which will configure for all relevant OSes. + */ +export const MalwareProtections = React.memo(() => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch(); + // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value + const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; + + const radios: Array<{ + id: ProtectionModes; + label: string; + protection: 'malware'; + }> = useMemo(() => { + return [ + { + id: ProtectionModes.detect, + label: i18n.translate('xpack.endpoint.policy.details.detect', { defaultMessage: 'Detect' }), + protection: 'malware', + }, + { + id: ProtectionModes.prevent, + label: i18n.translate('xpack.endpoint.policy.details.prevent', { + defaultMessage: 'Prevent', + }), + protection: 'malware', + }, + { + id: ProtectionModes.preventNotify, + label: i18n.translate('xpack.endpoint.policy.details.preventAndNotify', { + defaultMessage: 'Prevent and notify user', + }), + protection: 'malware', + }, + ]; + }, []); + + const handleSwitchChange = useCallback( + event => { + if (policyDetailsConfig) { + const newPayload = clone(policyDetailsConfig); + if (event.target.checked === false) { + for (const os of OSes) { + newPayload[os][protection].mode = ProtectionModes.off; + } + } else { + for (const os of OSes) { + newPayload[os][protection].mode = ProtectionModes.prevent; + } + } + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, + [dispatch, policyDetailsConfig] + ); + + const RadioButtons = () => { + return ( + <> + +
+ +
+
+ + + {radios.map(radio => { + return ( + + ); + })} + + + ); + }; + + const ProtectionSwitch = () => { + return ( + + ); + }; + + return ( + + ); +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts index c8e038869efcd..601f8a6bdc2c1 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts @@ -6,13 +6,13 @@ import { i18n } from '@kbn/i18n'; import { - EmbeddableFactory, IContainer, EmbeddableInput, + EmbeddableFactoryDefinition, } from '../../../../../../src/plugins/embeddable/public'; import { ResolverEmbeddable } from './embeddable'; -export class ResolverEmbeddableFactory extends EmbeddableFactory { +export class ResolverEmbeddableFactory implements EmbeddableFactoryDefinition { public readonly type = 'resolver'; public async isEditable() { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts index 0860c9c62aca4..a26f43e1f8cc0 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts @@ -45,7 +45,11 @@ interface AppRequestedResolverData { } /** - * When the user switches the active descendent of the Resolver. + * When the user switches the "active descendant" of the Resolver. + * The "active descendant" (from the point of view of the parent element) + * corresponds to the "current" child element. "active" or "current" here meaning + * the element that is focused on by the user's interactions with the UI, but + * not necessarily "selected" (see UserSelectedResolverNode below) */ interface UserFocusedOnResolverNode { readonly type: 'userFocusedOnResolverNode'; @@ -57,10 +61,27 @@ interface UserFocusedOnResolverNode { }; } +/** + * When the user "selects" a node in the Resolver + * "Selected" refers to the state of being the element that the + * user most recently "picked" (by e.g. pressing a button corresponding + * to the element in a list) as opposed to "active" or "current" (see UserFocusedOnResolverNode above). + */ +interface UserSelectedResolverNode { + readonly type: 'userSelectedResolverNode'; + readonly payload: { + /** + * Used to identify the process node that the user selected + */ + readonly nodeId: string; + }; +} + export type ResolverAction = | CameraAction | DataAction | UserBroughtProcessIntoView | UserChangedSelectedEvent | AppRequestedResolverData - | UserFocusedOnResolverNode; + | UserFocusedOnResolverNode + | UserSelectedResolverNode; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts index 1c66a998a4c22..82206d77f8349 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts @@ -4,18 +4,44 @@ * you may not use this file except in compliance with the Elastic License. */ import { Reducer, combineReducers } from 'redux'; +import { htmlIdGenerator } from '@elastic/eui'; import { animateProcessIntoView } from './methods'; import { cameraReducer } from './camera/reducer'; import { dataReducer } from './data/reducer'; import { ResolverState, ResolverAction, ResolverUIState } from '../types'; +import { uniquePidForProcess } from '../models/process_event'; + +/** + * Despite the name "generator", this function is entirely determinant + * (i.e. it will return the same html id given the same prefix 'resolverNode' + * and nodeId) + */ +const resolverNodeIdGenerator = htmlIdGenerator('resolverNode'); const uiReducer: Reducer = ( - uiState = { activeDescendentId: null }, + uiState = { activeDescendantId: null, selectedDescendantId: null }, action ) => { if (action.type === 'userFocusedOnResolverNode') { return { - activeDescendentId: action.payload.nodeId, + ...uiState, + activeDescendantId: action.payload.nodeId, + }; + } else if (action.type === 'userSelectedResolverNode') { + return { + ...uiState, + selectedDescendantId: action.payload.nodeId, + }; + } else if (action.type === 'userBroughtProcessIntoView') { + /** + * This action has a process payload (instead of a processId), so we use + * `uniquePidForProcess` and `resolverNodeIdGenerator` to resolve the determinant + * html id of the node being brought into view. + */ + const processNodeId = resolverNodeIdGenerator(uniquePidForProcess(action.payload.process)); + return { + ...uiState, + activeDescendantId: processNodeId, }; } else { return uiState; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index 37482916496e7..e8ae3d08e5cb6 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -6,6 +6,7 @@ import * as cameraSelectors from './camera/selectors'; import * as dataSelectors from './data/selectors'; +import * as uiSelectors from './ui/selectors'; import { ResolverState } from '../types'; /** @@ -59,6 +60,22 @@ export const processAdjacencies = composeSelectors( dataSelectors.processAdjacencies ); +/** + * Returns the id of the "current" tree node (fake-focused) + */ +export const uiActiveDescendantId = composeSelectors( + uiStateSelector, + uiSelectors.activeDescendantId +); + +/** + * Returns the id of the "selected" tree node (the node that is currently "pressed" and possibly controlling other popups / components) + */ +export const uiSelectedDescendantId = composeSelectors( + uiStateSelector, + uiSelectors.selectedDescendantId +); + /** * Returns the camera state from within ResolverState */ @@ -73,6 +90,13 @@ function dataStateSelector(state: ResolverState) { return state.data; } +/** + * Returns the ui state from within ResolverState + */ +function uiStateSelector(state: ResolverState) { + return state.ui; +} + /** * Whether or not the resolver is pending fetching data */ diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/ui/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/ui/selectors.ts new file mode 100644 index 0000000000000..196e834c406b3 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/ui/selectors.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; +import { ResolverUIState } from '../../types'; + +/** + * id of the "current" tree node (fake-focused) + */ +export const activeDescendantId = createSelector( + (uiState: ResolverUIState) => uiState, + /* eslint-disable no-shadow */ + ({ activeDescendantId }) => { + return activeDescendantId; + } +); + +/** + * id of the currently "selected" tree node + */ +export const selectedDescendantId = createSelector( + (uiState: ResolverUIState) => uiState, + /* eslint-disable no-shadow */ + ({ selectedDescendantId }) => { + return selectedDescendantId; + } +); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 674553aba0937..d370bda0d1842 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -37,7 +37,11 @@ export interface ResolverUIState { /** * The ID attribute of the resolver's aria-activedescendent. */ - readonly activeDescendentId: string | null; + readonly activeDescendantId: string | null; + /** + * The ID attribute of the resolver's currently selected descendant. + */ + readonly selectedDescendantId: string | null; } /** diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx index 911cda1be6517..8ee9bfafc630e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx @@ -193,6 +193,7 @@ export const SymbolIds = { runningTriggerCube: idGenerator('runningTriggerCube'), terminatedProcessCube: idGenerator('terminatedCube'), terminatedTriggerCube: idGenerator('terminatedTriggerCube'), + processCubeActiveBacking: idGenerator('activeBacking'), }; /** @@ -393,6 +394,15 @@ const SymbolsAndShapes = memo(() => ( /> + + resolver active backing + + )); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx index 58ce9b963de5d..36155ece57a9c 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -59,6 +59,7 @@ export const Resolver = styled( const { projectionMatrix, ref, onMouseDown } = useCamera(); const isLoading = useSelector(selectors.isLoading); + const activeDescendantId = useSelector(selectors.uiActiveDescendantId); useLayoutEffect(() => { dispatch({ @@ -66,6 +67,7 @@ export const Resolver = styled( payload: { selectedEvent }, }); }, [dispatch, selectedEvent]); + return (
{isLoading ? ( @@ -79,6 +81,7 @@ export const Resolver = styled( ref={ref} role="tree" tabIndex={0} + aria-activedescendant={activeDescendantId || undefined} > {edgeLineSegments.map(([startPosition, endPosition], index) => ( ({ left: `${left}px`, @@ -143,6 +148,9 @@ export const ProcessEventDot = styled( const labelId = useMemo(() => resolverNodeIdGenerator(), [resolverNodeIdGenerator]); const descriptionId = useMemo(() => resolverNodeIdGenerator(), [resolverNodeIdGenerator]); + const isActiveDescendant = nodeId === activeDescendantId; + const isSelectedDescendant = nodeId === selectedDescendantId; + const dispatch = useResolverDispatch(); const handleFocus = useCallback( @@ -153,16 +161,24 @@ export const ProcessEventDot = styled( nodeId, }, }); - focusEvent.currentTarget.setAttribute('aria-current', 'true'); }, [dispatch, nodeId] ); - const handleClick = useCallback(() => { - if (animationTarget.current !== null) { - animationTarget.current.beginElement(); - } - }, [animationTarget]); + const handleClick = useCallback( + (clickEvent: React.MouseEvent) => { + if (animationTarget.current !== null) { + (animationTarget.current as any).beginElement(); + } + dispatch({ + type: 'userSelectedResolverNode', + payload: { + nodeId, + }, + }); + }, + [animationTarget, dispatch, nodeId] + ); return ( @@ -179,6 +195,8 @@ export const ProcessEventDot = styled( aria-labelledby={labelId} aria-describedby={descriptionId} aria-haspopup={'true'} + aria-current={isActiveDescendant ? 'true' : undefined} + aria-selected={isSelectedDescendant ? 'true' : undefined} style={nodeViewportStyle} id={nodeId} onClick={handleClick} @@ -186,6 +204,15 @@ export const ProcessEventDot = styled( tabIndex={-1} > + + = { diff --git a/x-pack/plugins/endpoint/scripts/mapping.json b/x-pack/plugins/endpoint/scripts/mapping.json index 34c039d643517..5878e01b52a47 100644 --- a/x-pack/plugins/endpoint/scripts/mapping.json +++ b/x-pack/plugins/endpoint/scripts/mapping.json @@ -90,7 +90,7 @@ } } }, - "malware_classifier": { + "malware_classification": { "properties": { "features": { "properties": { @@ -452,7 +452,7 @@ } } }, - "malware_classifier": { + "malware_classification": { "properties": { "features": { "properties": { @@ -849,7 +849,7 @@ } } }, - "malware_classifier": { + "malware_classification": { "properties": { "features": { "properties": { @@ -1494,7 +1494,7 @@ } } }, - "malware_classifier": { + "malware_classification": { "properties": { "features": { "properties": { @@ -1687,7 +1687,7 @@ } } }, - "malware_classifier": { + "malware_classification": { "properties": { "features": { "properties": { diff --git a/x-pack/plugins/endpoint/scripts/resolver_generator.ts b/x-pack/plugins/endpoint/scripts/resolver_generator.ts index 3d11ccaad005d..aebf92eff6cb8 100644 --- a/x-pack/plugins/endpoint/scripts/resolver_generator.ts +++ b/x-pack/plugins/endpoint/scripts/resolver_generator.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import * as yargs from 'yargs'; +import seedrandom from 'seedrandom'; import { Client, ClientOptions } from '@elastic/elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; -import { EndpointDocGenerator } from '../common/generate_data'; +import { EndpointDocGenerator, Event } from '../common/generate_data'; import { default as mapping } from './mapping.json'; main(); @@ -137,14 +138,24 @@ async function main() { // eslint-disable-next-line no-console console.log('No seed supplied, using random seed: ' + seed); } - const generator = new EndpointDocGenerator(seed); + const random = seedrandom(seed); for (let i = 0; i < argv.numHosts; i++) { - await client.index({ - index: argv.metadataIndex, - body: generator.generateHostMetadata(), - }); + const generator = new EndpointDocGenerator(random); + const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents + const numMetadataDocs = 5; + const timestamp = new Date().getTime(); + for (let j = 0; j < numMetadataDocs; j++) { + generator.updateHostData(); + await client.index({ + index: argv.metadataIndex, + body: generator.generateHostMetadata( + timestamp - timeBetweenDocs * (numMetadataDocs - j - 1) + ), + }); + } + for (let j = 0; j < argv.alertsPerHost; j++) { - const resolverDocs = generator.generateFullResolverTree( + const resolverDocGenerator = generator.fullResolverTreeGenerator( argv.ancestors, argv.generations, argv.children, @@ -152,15 +163,23 @@ async function main() { argv.percentWithRelated, argv.percentTerminated ); - const body = resolverDocs.reduce( - (array: Array>, doc) => ( - array.push({ index: { _index: argv.eventIndex } }, doc), array - ), - [] - ); - - await client.bulk({ body }); + let result = resolverDocGenerator.next(); + while (!result.done) { + let k = 0; + const resolverDocs: Event[] = []; + while (k < 1000 && !result.done) { + resolverDocs.push(result.value); + result = resolverDocGenerator.next(); + k++; + } + const body = resolverDocs.reduce( + (array: Array>, doc) => ( + array.push({ index: { _index: argv.eventIndex } }, doc), array + ), + [] + ); + await client.bulk({ body }); + } } - generator.randomizeHostData(); } } diff --git a/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json b/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json index 3c824185ec083..3c8486aa127ea 100644 --- a/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json +++ b/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json @@ -23,6 +23,11 @@ "event" : { "created" : "2020-01-23T21:56:55.336Z" }, + "elastic": { + "agent": { + "id": "56a75650-3c8a-4e4f-ac17-6dd729c650e2" + } + }, "endpoint" : { "policy" : { "id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" @@ -73,6 +78,11 @@ "event" : { "created" : "2020-01-23T21:56:55.336Z" }, + "elastic": { + "agent": { + "id": "56a75650-3c8a-4e4f-ac17-6dd729c650e2" + } + }, "endpoint" : { "policy" : { "id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" @@ -115,6 +125,11 @@ "event" : { "created" : "2020-01-23T21:56:55.336Z" }, + "elastic": { + "agent": { + "id": "c2d84d8f-d355-40de-8b54-5d318d4d1312" + } + }, "endpoint" : { "policy" : { "id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" @@ -165,6 +180,11 @@ "event" : { "created" : "2020-01-23T21:56:55.336Z" }, + "elastic": { + "agent": { + "id": "c2d84d8f-d355-40de-8b54-5d318d4d1312" + } + }, "endpoint" : { "policy" : { "id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index 50e7fdd5a9048..d0e4652c2828e 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -8,7 +8,7 @@ "ignore_above": 1024, "type": "keyword", "meta": { - "isArray": true + "isArray": "true" } }, "message": { diff --git a/x-pack/plugins/event_log/scripts/create_schemas.js b/x-pack/plugins/event_log/scripts/create_schemas.js index b46f7f295ddc7..2432a27e5c70d 100755 --- a/x-pack/plugins/event_log/scripts/create_schemas.js +++ b/x-pack/plugins/event_log/scripts/create_schemas.js @@ -117,7 +117,7 @@ function augmentMappings(mappings, multiValuedProperties) { const fullProp = replaceDotWithProperties(prop); const metaPropName = `${fullProp}.meta`; const meta = lodash.get(mappings.properties, metaPropName) || {}; - meta.isArray = true; + meta.isArray = 'true'; lodash.set(mappings.properties, metaPropName, meta); } } @@ -127,7 +127,7 @@ function generateSchemaLines(lineWriter, prop, mappings) { if (mappings == null) return; if (StringTypes.has(mappings.type)) { - if (mappings.meta && mappings.meta.isArray) { + if (mappings.meta && mappings.meta.isArray === 'true') { lineWriter.addLine(`${propKey}: ecsStringMulti(),`); } else { lineWriter.addLine(`${propKey}: ecsString(),`); diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index 4cace59752f36..f606bb2be6c6c 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -13,7 +13,7 @@ import { IEvent } from '../generated/schemas'; export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), logEntries: schema.boolean({ defaultValue: false }), - indexEntries: schema.boolean({ defaultValue: false }), + indexEntries: schema.boolean({ defaultValue: true }), }); export type IEventLogConfig = TypeOf; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts index 7e3e1fba9c44a..397a78354f470 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts @@ -16,7 +16,7 @@ import { import { IndexManagementHome } from '../../../public/application/sections/home'; // eslint-disable-line @kbn/eslint/no-restricted-paths import { BASE_PATH } from '../../../common/constants'; import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths -import { Template } from '../../../common/types'; +import { TemplateDeserialized } from '../../../common'; import { WithAppDependencies, services } from './setup_environment'; const testBedConfig: TestBedConfig = { @@ -36,10 +36,13 @@ export interface IdxMgmtHomeTestBed extends TestBed { selectHomeTab: (tab: 'indicesTab' | 'templatesTab') => void; selectDetailsTab: (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => void; clickReloadButton: () => void; - clickTemplateAction: (name: Template['name'], action: 'edit' | 'clone' | 'delete') => void; + clickTemplateAction: ( + name: TemplateDeserialized['name'], + action: 'edit' | 'clone' | 'delete' + ) => void; clickTemplateAt: (index: number) => void; clickCloseDetailsButton: () => void; - clickActionMenu: (name: Template['name']) => void; + clickActionMenu: (name: TemplateDeserialized['name']) => void; }; } @@ -78,7 +81,7 @@ export const setup = async (): Promise => { find('reloadButton').simulate('click'); }; - const clickActionMenu = async (templateName: Template['name']) => { + const clickActionMenu = async (templateName: TemplateDeserialized['name']) => { const { component } = testBed; // When a table has > 2 actions, EUI displays an overflow menu with an id "-actions" @@ -87,7 +90,7 @@ export const setup = async (): Promise => { }; const clickTemplateAction = ( - templateName: Template['name'], + templateName: TemplateDeserialized['name'], action: 'edit' | 'clone' | 'delete' ) => { const actions = ['edit', 'clone', 'delete']; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts index 9d4eb631a1c40..520b62083e7d3 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { TestBed, SetupFunc, UnwrapPromise } from '../../../../../test_utils'; -import { Template } from '../../../common/types'; +import { TemplateDeserialized } from '../../../common'; import { nextTick } from './index'; interface MappingField { @@ -62,8 +62,8 @@ export const formSetup = async (initTestBed: SetupFunc) => { indexPatterns, order, version, - }: Partial