diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index 6b8dc31bab34ed..f2a58e7b6a7ac5 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 5c13ccccd9c6f7..815c1345bbb680 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 a00bcb3bbc9465..a3470cd750738d 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 ce472a404c0538..ade79f27e10e96 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 6f147d0e3223ac..0d2834c1a400ef 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 d998b5daedd9b0..ded0553219a031 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 b871131805ab53..445f1f5dd8df5b 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 bc705b66e17d14..179dbc66eb1940 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 60695c0e3f1cf4..416e95a8941ce8 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 d47581ffe720ac..793f09ea83b4f5 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 d2ebe003afd6be..1254f412e14c52 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/user/alerting/images/alert-concepts-summary.svg b/docs/user/alerting/images/alert-concepts-summary.svg index d11023b7064188..0d63601c0693d8 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 b7b0bd49960532..97f610041f0eb5 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 2995c99ac9e58a..b81a229ea23e72 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 247cf48b41bde2..1fde254110c624 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 9afdeabaee7656..d010e25885ce68 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 79859cc015a80c..382bb65e769ef2 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 d7be436905382f..bc577ca36d793d 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 ce92301236c2b8..2af6c713593c68 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 49b5baecda474a..46e0b9adfea251 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", diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 9337daf419bfac..a3a11783cd82a7 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 bcebdf591d6f03..5ea031595d1d46 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 5028c6efdb40eb..f19271de8ad278 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 7ee96050a1248a..dec519da696414 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 d1bb93ddecd0a4..666ec7a46ff06e 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 c76e909d2adbcb..e2823f23d04317 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 dc6e7ae33dbecd..a8752745449055 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 00000000000000..3f82ebc9cdbae3 --- /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/elasticsearch/elasticsearch_config.test.mocks.ts b/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts index d908fdbfd2e807..df8566746eaf9a 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 1b4fc5eafec763..de3f57298f461f 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 b2f4e388b337d3..d3012e361b3ed0 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 ab98c3a27920c5..50e34c7c8f6782 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 3980b9c247fa33..5d0bed601f5408 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 0096eeb0925658..4eb0c50e72362f 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 c6c28697c4bccf..e9eb72fe395ffd 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 86924c559e5fa0..b01a4c4e048998 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 e35356343cfe24..a6df0992f6cc67 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 a39266ecd8db38..e38345989598d8 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 08ccc1e0d1e89d..14af89f80f9aab 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 171f08b45cf8d5..9b8f75bdcf9535 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 b4c9e939d3083a..e9ebe73c3b34dc 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 00000000000000..70a9d86206fd6b --- /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 a4373dcdfe1d14..2489867fa62334 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/discover_state.test.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts index 3840fd0c2e3bed..b7b36ca960167e 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 d9e1850cd6a249..2a036f0ac60ad3 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 6f3adc1f4fcce6..ad61984a525365 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 2ed7e3d43168c8..addc608efd57d8 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 d1bf4411cac2a7..7c9ab32ab2f72c 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 6d32579f5c5412..a14c4a44f1c7c9 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/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index 106dbcd9f8ab2c..ad4aa97d8ea7a0 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 99560b0bf653f3..0912d8683fc485 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 53b8f9983aa270..00000000000000 --- 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 00000000000000..df44ea45e9d01c --- /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 e9472fbdee0e67..04eeff1e1f3ce3 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/dashboard/public/actions/expand_panel_action.test.tsx b/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx index 22cf854a46623e..e9696938b86292 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 69346dc8c118a6..2252928f46f6a1 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 770c46c62e42f6..6a734cb68fd9c3 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 0fa62fc8756037..9ff48cb45adfda 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 0f1b9c6dc93072..a946c21765311d 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 e3d9b8552f0606..be4d2e3851f11c 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 a81d80b440e049..1c72ad34e54468 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 0c02b02a25af0d..ef6eaa196b06a1 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 ef1d89e400b729..857c8594720ee6 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 eca74af4ec2530..23275fbe8e8f02 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 9aeaf34f3311b8..ce733bba6dda52 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 07915ce59e6ca2..9e47da5cea0329 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 00000000000000..570a78fc41ea91 --- /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 eb10c16806640e..a135484ff61be0 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 81f7f35c900c98..7949b6fb8ba27a 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 00000000000000..b8985f7311ea9e --- /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 51b83ea0ecaa34..e27045495af5bb 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 2175c3a59aa586..4d6ab37a50c05c 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 757d4e6bfddefb..649677dc67c7d1 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 8ee8c8dad9df36..74b08535bf27a5 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 2fa21e40ca0f07..282b0f05891e02 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 95eeb63710c32e..06c47bd1bcad8a 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 3f7c917cd16173..2f66d8eb0d619a 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 e19acda8419da7..ee31127cb5a405 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 f4d5aa148373bd..dea4a88bda0823 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 20a5a8112f4d36..f977329562b9b6 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 3488f6a2e038d2..f27c7e8b011fdc 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 f37a16ea86c431..4c941ee22abfaa 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 ba2f78e42e10eb..2ee05d8316aced 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 c334411004e2c1..804f3e2e8a7b4c 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 381665c359ffd2..a483f90f76dde7 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 6beef35bbe1368..54f3ac2887f6c6 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 1ee52f47491352..87076399465d35 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 99d5a7c747d15b..19e461b8bde7e8 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 f0a7219531b596..0e03db3ec83584 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 1a222cd548de78..1989d6356cbd14 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 7a879c389f3c44..2d112b23598180 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 6771abd64df7e0..06af698f2ce023 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 5626fc80bb749a..dc8321aa07004a 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 a8d24984cec7cf..0509b8081c35be 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 57cbe0f17498fd..c1dc560b4353f8 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 2e979bf5899755..7d827b1ca9229f 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 77cde6a574aece..5263dc06e96fc8 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 78f9827ffde3e7..3753c4dbbb9ed5 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 0c7e732f0b1850..e64d2002517977 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 f4983a4313c4d6..2aa346423297a3 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 d3e7b759a44166..216defcee90161 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 24e97aa081e510..4e106ef3e039ab 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 ac2921ed063f50..9e75250403d6b1 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/dashboard/bwc_shared_urls.js b/test/functional/apps/dashboard/bwc_shared_urls.js index fb1e580135e5a7..b56cb658b80bb7 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 2e7b7f9a6dbb16..5a2628f42ded5d 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 f0a7844b299873..08e0cb0b8d23a7 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 24f4ba592324c0..f8f290b259b7eb 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 c99368ba4e8599..97337d4573e2a7 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/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 10652ce3ec4b2d..2377c32a80b5b6 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 6225b4e3aca62f..6fdc306e39192d 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/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 f8625e4490e511..fd07416cadbc5d 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 c5638f2624fe5d..1c55c676d94253 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/canvas/public/components/workpad_header/workpad_export/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts index 949264fcc9fdb1..b0083eb4f87e27 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 ceaf82c1c07d62..6c7d7ddd0a7931 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 5adbf4ce66c130..dc99c0687f388e 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 1caea1b4b728f5..99a59c756e2281 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/embeddable/README.md b/x-pack/legacy/plugins/maps/public/embeddable/README.md index 1de327702fb87a..8ce3794e2ed2c5 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 3c9069c7a836f0..9544e8714f265f 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 b9cb66f8312814..5a036ed47fb622 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/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 bf57306df5697d..eadaf42ca694da 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 059543d705fc73..20fd97a229352c 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 edf230b0a945c0..0656173e5c4113 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 0f6a0583d3dbcc..4e2d07b9dfea02 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 5e8f720fcc5e35..460e7379920c46 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 00000000000000..b4fad6690b9ac8 --- /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 ab1634a53a966a..a295556ee3126d 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 146bc40aa8531b..e671f00b783819 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/reporting/common/constants.ts b/x-pack/legacy/plugins/reporting/common/constants.ts index 1746345879192e..8f7a06ba9f8e92 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 468caf93ec5dd5..9085fb3cbc876d 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 436b2c2dab1ad5..6f415d7ee5ea93 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 eedb742ad75976..5f5fc94eee8308 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 975060a8052f07..bd7999d697ca9d 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 fa53f474dfba7b..2cbde69c81316f 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 7af5edab41ab77..a13f992e7867cd 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 27e772195f7260..5f55617724ff68 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 ca64d8632dbfeb..c4b6f31019fdf4 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 f446369fec78ce..abf5784dacff9f 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 cbebd6bc21b0e6..2fbfd868674f60 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 0cb83352d4606a..07fceb603e451e 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 6007c2960057a2..f6974379253fb9 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 16eb433e8a75e3..57d025890d3e22 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 13d07bcdd6baf7..75ac3dca4ffa06 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 44c04c763f840a..53a11c18abd797 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 fbae1f91a7a6a7..a484dfb243563d 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 ab81a952f345ce..76613c2d631d64 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 2f6dc2829dfd8d..069896c8d9e90c 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 93ad40026dff81..7960e1552e5590 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 7ea67277015ab6..0e704a041452ab 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 f12916b734dbf6..93dbe598b367c9 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 15799858910532..d78d8a8a8010d8 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 842330fa7c93f3..529c195486bc6d 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 17072d311b35f3..8e0376a190267a 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 6bb3e73fcfe84a..afa917f17651c3 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 6a7d5f336e238d..ab14d2dd8a660c 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 a6911e1f147040..1f834bde88a2de 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 e2e6ba1b890963..cb63e7dad2fdf4 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 8670f0027af89e..113da92d1862f0 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 88e91982adc632..a15541d99f6fb5 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 656c99991e1f61..25d2d64b1029d7 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 484842ba18f2ad..c6f07f8ad2d344 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 535c2dcd439a7a..dbdccb6160a6e6 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 d78effaa1fc2f9..a62b7ec7013a59 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 0a9dcfe986ca63..e8dd3c5207d926 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 89e98302cddc91..a5d27d0545da10 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 b07475df6304ff..7aaed2038bd523 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 dc79a6b9db2c11..a2f7a1f3ad0dae 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 f90f2c7aee395b..cb228150efbcd0 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 d32338ae3e311e..5f89662c94da2e 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 49c6222c9f276f..af3b86919dc508 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 73186966e3d2f3..3697c4b86ce3ce 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 b36345c08bfee9..9714c5965a5db2 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 0c480fc82752bc..f096073ec2f5f1 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 00000000000000..623d3c2015f3b2 --- /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 4506d41e4f5c3e..9be61d091b00e8 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 24e2a954415d9c..c564963e363cc2 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 336ff5f4d2ee7c..679b42aca6de5a 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 d593e4625cdf48..8230ee889ae057 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 d4d913243e18d8..ad8db3201844e0 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 35677123676081..16b8fbdb30fdd9 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 dbc01fc947f8b6..97876529ecfa71 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 c215bdc3989045..5a062a693b4681 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 49d5c568c39818..5e73fe77ecb79d 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 0a2db749cb954a..f5ccbe493a91f0 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 c01e6377b039e5..0affc111c13685 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 10980f702d8493..00000000000000 --- 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 04f998fd3e5a5d..00000000000000 --- 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 0fdbd858b8e3c7..85d9f727d7fa7b 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 89c49123e85bf2..d6512d5eb718b8 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 e0af94cbdc29cf..00000000000000 --- 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 942dcaf842696c..2551fd48b91f34 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 ce4a5b93e74310..a20905ba093d4e 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 f4f4d61246b6ae..00000000000000 --- 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 4f24cc16b2277c..c9ed2e81c6792a 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 56622617586f7d..6b4f5dbd9203a3 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 415b6b7d643669..830953d5322431 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 5d17fa2e82b8c6..519e49f56c3778 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 54d9671692c5de..8e54feac3c8a6e 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 096ba84b63d1ac..1c6129313db4b4 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 071b401d2321bc..9f0de844df3699 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 b9aa75e0ddd000..f6f98b2377db6b 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 3460d22592e3db..b5d6ae59ce5dda 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 c5f8c78016f618..1ca28ca62a7f28 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 fb3944ea33552f..aef37754681ec9 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 30627d5b232301..e7e7c866db96a0 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 9e618ff1fe40a2..8a79566aafae29 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 3d275d34e2f7d6..06f7efaa9dcbbf 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 59b7bc2020ad93..bec00688432cc4 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 bd2d0cb835a790..e9523d9e702029 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 a6d753f9b107a2..929109e66914d9 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 14202530fb6c7b..8f9d65c200dade 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 883276d43e27e0..930aa7601b8cbc 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 0250e6c0a9afdb..be60b56dcc0c17 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 2cd129d47b3f96..34ff91d1972a08 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 bb7851ba036a90..531e1dcaf84e0f 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 238079ba92a291..09d53278941c91 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 646132c3f88ebb..f38cb2285b4803 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 f388ac1215d01f..cb776be8d7b6bb 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 c7e368da1338f6..cbb4006bbf9334 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 0ffb13cd660286..f4e6ee5f878a63 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 56211c9ff8935b..0c7a1212ba280f 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 05dfd561b1f5ec..b00eef79ee480c 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 426a1ab9238dc4..4d9e283bfb9cc2 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 0dd95bea8a0b24..44d5de10e361a0 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 0d37cce1fd85ca..412fc0706b1517 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 37e93b1481e15f..6b0c7e0078268e 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 d90f94d32001da..4e97c597546a7a 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 26827429604421..c248223c6b81be 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 c58e62c062faec..140dd1544b12b0 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 a96913f2ad541f..9e45371fb6058e 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 5157bd81403e28..9a84d33ab5fdf4 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 1a982725464402..ccdfd1ed1be38d 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 0fd07f30a00b67..31aaa426e4f3b8 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 65a606604d4a7c..1bee36ed9e1850 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 e1ca84ed8cc642..a7ce0c85ffdcf7 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 03352166729154..2686bb47925b6c 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 b8e2310ef06146..cb4d88a8bb539c 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 60d6158987a1db..c42e7b902cd5c5 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 522464d585ccae..443dbd2c93a35d 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 b6afba527ccdcf..db1f2298b5ea78 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 2b93ec8b101120..8831bc77691fa7 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 8e4b5ce3c99242..bdbb6ff7d1052b 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 d5f9d332bbcddb..cc97a1f8a9f0b2 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 91905722fbca31..246701e94c99a2 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 6552f973a66fa9..fc87a775a9e68f 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 5596d0c70f5ea3..f69a715f9b2c99 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 757c1eb557c543..8d44c170182551 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 dc73dc28718854..00000000000000 --- 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 964eb8137685f6..00000000000000 --- 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/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 89227cdd56457d..85d0b1b593704a 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/package.json b/x-pack/package.json index bbab1a96f52f43..24b23256bf18ea 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 91b829a020048a..10860fe471a3bc 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 d2b9fa9ac16555..3bf763470f0026 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 311d3357476b91..e8d9451f9f2a6d 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 81adf76ac4ce9c..4b74b07fc8e27d 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 cfe4aa01a4a997..152db37a1bff32 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 b0255d2d828bb3..7fa44b8c85f410 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 7331b6c5dcbf5e..bb050076b9f9a9 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 00000000000000..40b220ffe68f11 --- /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 4a8608199c0376..75c1c945c5d261 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 dfb906c7af6064..88e1c66ea3e821 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 430ba1d422b96e..0ec105129b7ac4 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -100,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 { @@ -139,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, @@ -149,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), @@ -183,7 +204,7 @@ export class EndpointDocGenerator { trusted: false, subject_name: 'bad signer', }, - malware_classifier: { + malware_classification: { identifier: 'endpointpe', score: 1, threshold: 0.66, @@ -241,7 +262,7 @@ export class EndpointDocGenerator { sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', }, - malware_classifier: { + malware_classification: { identifier: 'Whitelisted', score: 0, threshold: 0, @@ -255,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(), @@ -277,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, @@ -295,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 }); @@ -321,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 565f47e7a0d6fb..e8e1281a889253 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; 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 e1ac9defc858eb..9ac53f9be609ff 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 4215edb4d68108..d4f6d2457254e2 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 0183e9663bb444..79cb61693056cf 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 4134bc35747d64..538562bfbbc064 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 b900a0a35dbf57..6a6936c59a00ef 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 f2c79155f3c23f..2dba301bf45376 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 e92e22fc97fe6e..7bec2c4c742d2b 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 00000000000000..66b22178607b94 --- /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 c8e038869efcd6..601f8a6bdc2c1b 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 0860c9c62aca47..a26f43e1f8cc08 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 1c66a998a4c228..82206d77f83490 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 37482916496e75..e8ae3d08e5cb61 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 00000000000000..196e834c406b31 --- /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 674553aba09372..d370bda0d18424 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 911cda1be65170..8ee9bfafc630e6 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 58ce9b963de5d8..36155ece57a9c4 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 34c039d6435171..5878e01b52a47d 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 3d11ccaad005d4..aebf92eff6cb81 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/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index 50e7fdd5a90483..d0e4652c2828ec 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 b46f7f295ddc7b..2432a27e5c70d2 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 4cace59752f366..f606bb2be6c6c1 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 7e3e1fba9c44a6..397a78354f4707 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 9d4eb631a1c408..520b62083e7d3d 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